mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-25 02:46:17 +04:00
Prevent preview binary or large file (#939)
* Prevent preview binary or large file (#847) * fix wrong method name * fix add use trait * update lock file * rename MAX_PREVIEW_SIZE from MAX_BYTE_PREVIEW * read small bytes to determine cotent type * [WIP] add preview struct to represent calcurated preveiw * Refactor content type detection - Remove unwraps - Reuse a single read buffer to avoid 1kb reallocations between previews * Refactor preview rendering so we don't construct docs when not necessary * Replace unwarap whit Preview::NotFound * Use index access to hide unwrap Co-authored-by: Blaž Hrastnik <blaz@mxxn.io> * fix Get and unwarp equivalent to referce of Index acess * better preview implementation * Rename Preview enum and vairant Co-authored-by: Gokul Soumya <gokulps15@gmail.com> * fixup! Rename Preview enum and vairant * simplify long match * Center text, add docs, fix formatting, refactor Co-authored-by: Blaž Hrastnik <blaz@mxxn.io> Co-authored-by: Gokul Soumya <gokulps15@gmail.com>
This commit is contained in:
parent
5b5d1b9dff
commit
70d21a903f
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -110,6 +110,15 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "content_inspector"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.5"
|
||||
@ -415,6 +424,7 @@ version = "0.5.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"content_inspector",
|
||||
"crossterm",
|
||||
"fern",
|
||||
"futures-util",
|
||||
|
@ -46,6 +46,8 @@ fuzzy-matcher = "0.3"
|
||||
ignore = "0.4"
|
||||
# markdown doc rendering
|
||||
pulldown-cmark = { version = "0.8", default-features = false }
|
||||
# file type detection
|
||||
content_inspector = "0.2.4"
|
||||
|
||||
# config
|
||||
toml = "0.5"
|
||||
|
@ -12,7 +12,12 @@
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
use tui::widgets::Widget;
|
||||
|
||||
use std::{borrow::Cow, collections::HashMap, path::PathBuf};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashMap,
|
||||
io::Read,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::ui::{Prompt, PromptEvent};
|
||||
use helix_core::Position;
|
||||
@ -23,18 +28,58 @@
|
||||
};
|
||||
|
||||
pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80;
|
||||
/// Biggest file size to preview in bytes
|
||||
pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
|
||||
|
||||
/// File path and line number (used to align and highlight a line)
|
||||
/// File path and range of lines (used to align and highlight lines)
|
||||
type FileLocation = (PathBuf, Option<(usize, usize)>);
|
||||
|
||||
pub struct FilePicker<T> {
|
||||
picker: Picker<T>,
|
||||
/// Caches paths to documents
|
||||
preview_cache: HashMap<PathBuf, Document>,
|
||||
preview_cache: HashMap<PathBuf, CachedPreview>,
|
||||
read_buffer: Vec<u8>,
|
||||
/// Given an item in the picker, return the file path and line number to display.
|
||||
file_fn: Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>,
|
||||
}
|
||||
|
||||
pub enum CachedPreview {
|
||||
Document(Document),
|
||||
Binary,
|
||||
LargeFile,
|
||||
NotFound,
|
||||
}
|
||||
|
||||
// We don't store this enum in the cache so as to avoid lifetime constraints
|
||||
// from borrowing a document already opened in the editor.
|
||||
pub enum Preview<'picker, 'editor> {
|
||||
Cached(&'picker CachedPreview),
|
||||
EditorDocument(&'editor Document),
|
||||
}
|
||||
|
||||
impl Preview<'_, '_> {
|
||||
fn document(&self) -> Option<&Document> {
|
||||
match self {
|
||||
Preview::EditorDocument(doc) => Some(doc),
|
||||
Preview::Cached(CachedPreview::Document(doc)) => Some(doc),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Alternate text to show for the preview.
|
||||
fn placeholder(&self) -> &str {
|
||||
match *self {
|
||||
Self::EditorDocument(_) => "<File preview>",
|
||||
Self::Cached(preview) => match preview {
|
||||
CachedPreview::Document(_) => "<File preview>",
|
||||
CachedPreview::Binary => "<Binary file>",
|
||||
CachedPreview::LargeFile => "<File too large to preview>",
|
||||
CachedPreview::NotFound => "<File not found>",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FilePicker<T> {
|
||||
pub fn new(
|
||||
options: Vec<T>,
|
||||
@ -45,6 +90,7 @@ pub fn new(
|
||||
Self {
|
||||
picker: Picker::new(false, options, format_fn, callback_fn),
|
||||
preview_cache: HashMap::new(),
|
||||
read_buffer: Vec::with_capacity(1024),
|
||||
file_fn: Box::new(preview_fn),
|
||||
}
|
||||
}
|
||||
@ -60,14 +106,45 @@ fn current_file(&self, editor: &Editor) -> Option<FileLocation> {
|
||||
})
|
||||
}
|
||||
|
||||
fn calculate_preview(&mut self, editor: &Editor) {
|
||||
if let Some((path, _line)) = self.current_file(editor) {
|
||||
if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() {
|
||||
// TODO: enable syntax highlighting; blocked by async rendering
|
||||
let doc = Document::open(&path, None, Some(&editor.theme), None).unwrap();
|
||||
self.preview_cache.insert(path, doc);
|
||||
}
|
||||
/// Get (cached) preview for a given path. If a document corresponding
|
||||
/// to the path is already open in the editor, it is used instead.
|
||||
fn get_preview<'picker, 'editor>(
|
||||
&'picker mut self,
|
||||
path: &Path,
|
||||
editor: &'editor Editor,
|
||||
) -> Preview<'picker, 'editor> {
|
||||
if let Some(doc) = editor.document_by_path(path) {
|
||||
return Preview::EditorDocument(doc);
|
||||
}
|
||||
|
||||
if self.preview_cache.contains_key(path) {
|
||||
return Preview::Cached(&self.preview_cache[path]);
|
||||
}
|
||||
|
||||
let data = std::fs::File::open(path).and_then(|file| {
|
||||
let metadata = file.metadata()?;
|
||||
// Read up to 1kb to detect the content type
|
||||
let n = file.take(1024).read_to_end(&mut self.read_buffer)?;
|
||||
let content_type = content_inspector::inspect(&self.read_buffer[..n]);
|
||||
self.read_buffer.clear();
|
||||
Ok((metadata, content_type))
|
||||
});
|
||||
let preview = data
|
||||
.map(
|
||||
|(metadata, content_type)| match (metadata.len(), content_type) {
|
||||
(_, content_inspector::ContentType::BINARY) => CachedPreview::Binary,
|
||||
(size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => CachedPreview::LargeFile,
|
||||
_ => {
|
||||
// TODO: enable syntax highlighting; blocked by async rendering
|
||||
Document::open(path, None, Some(&editor.theme), None)
|
||||
.map(CachedPreview::Document)
|
||||
.unwrap_or(CachedPreview::NotFound)
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap_or(CachedPreview::NotFound);
|
||||
self.preview_cache.insert(path.to_owned(), preview);
|
||||
Preview::Cached(&self.preview_cache[path])
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,12 +156,12 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
// |picker | | |
|
||||
// | | | |
|
||||
// +---------+ +---------+
|
||||
self.calculate_preview(cx.editor);
|
||||
let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
|
||||
let area = inner_rect(area);
|
||||
// -- Render the frame:
|
||||
// clear area
|
||||
let background = cx.editor.theme.get("ui.background");
|
||||
let text = cx.editor.theme.get("ui.text");
|
||||
surface.clear_with(area, background);
|
||||
|
||||
let picker_width = if render_preview {
|
||||
@ -113,17 +190,23 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
horizontal: 1,
|
||||
};
|
||||
let inner = inner.inner(&margin);
|
||||
|
||||
block.render(preview_area, surface);
|
||||
|
||||
if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, range)| {
|
||||
cx.editor
|
||||
.document_by_path(&path)
|
||||
.or_else(|| self.preview_cache.get(&path))
|
||||
.zip(Some(range))
|
||||
}) {
|
||||
if let Some((path, range)) = self.current_file(cx.editor) {
|
||||
let preview = self.get_preview(&path, cx.editor);
|
||||
let doc = match preview.document() {
|
||||
Some(doc) => doc,
|
||||
None => {
|
||||
let alt_text = preview.placeholder();
|
||||
let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2;
|
||||
let y = inner.y + inner.height / 2;
|
||||
surface.set_stringn(x, y, alt_text, inner.width as usize, text);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// align to middle
|
||||
let first_line = line
|
||||
let first_line = range
|
||||
.map(|(start, end)| {
|
||||
let height = end.saturating_sub(start) + 1;
|
||||
let middle = start + (height.saturating_sub(1) / 2);
|
||||
@ -150,7 +233,7 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
);
|
||||
|
||||
// highlight the line
|
||||
if let Some((start, end)) = line {
|
||||
if let Some((start, end)) = range {
|
||||
let offset = start.saturating_sub(first_line) as u16;
|
||||
surface.set_style(
|
||||
Rect::new(
|
||||
|
Loading…
Reference in New Issue
Block a user