mirror of https://github.com/helix-editor/helix
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>pull/950/head
parent
5b5d1b9dff
commit
70d21a903f
|
@ -110,6 +110,15 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "content_inspector"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
|
@ -415,6 +424,7 @@ version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"content_inspector",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"fern",
|
"fern",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
|
|
@ -46,6 +46,8 @@ fuzzy-matcher = "0.3"
|
||||||
ignore = "0.4"
|
ignore = "0.4"
|
||||||
# markdown doc rendering
|
# markdown doc rendering
|
||||||
pulldown-cmark = { version = "0.8", default-features = false }
|
pulldown-cmark = { version = "0.8", default-features = false }
|
||||||
|
# file type detection
|
||||||
|
content_inspector = "0.2.4"
|
||||||
|
|
||||||
# config
|
# config
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
|
|
|
@ -12,7 +12,12 @@ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
|
||||||
use fuzzy_matcher::FuzzyMatcher;
|
use fuzzy_matcher::FuzzyMatcher;
|
||||||
use tui::widgets::Widget;
|
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 crate::ui::{Prompt, PromptEvent};
|
||||||
use helix_core::Position;
|
use helix_core::Position;
|
||||||
|
@ -23,18 +28,58 @@ use helix_view::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80;
|
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)>);
|
type FileLocation = (PathBuf, Option<(usize, usize)>);
|
||||||
|
|
||||||
pub struct FilePicker<T> {
|
pub struct FilePicker<T> {
|
||||||
picker: Picker<T>,
|
picker: Picker<T>,
|
||||||
/// Caches paths to documents
|
/// 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.
|
/// Given an item in the picker, return the file path and line number to display.
|
||||||
file_fn: Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>,
|
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> {
|
impl<T> FilePicker<T> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
options: Vec<T>,
|
options: Vec<T>,
|
||||||
|
@ -45,6 +90,7 @@ impl<T> FilePicker<T> {
|
||||||
Self {
|
Self {
|
||||||
picker: Picker::new(false, options, format_fn, callback_fn),
|
picker: Picker::new(false, options, format_fn, callback_fn),
|
||||||
preview_cache: HashMap::new(),
|
preview_cache: HashMap::new(),
|
||||||
|
read_buffer: Vec::with_capacity(1024),
|
||||||
file_fn: Box::new(preview_fn),
|
file_fn: Box::new(preview_fn),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,14 +106,45 @@ impl<T> FilePicker<T> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_preview(&mut self, editor: &Editor) {
|
/// Get (cached) preview for a given path. If a document corresponding
|
||||||
if let Some((path, _line)) = self.current_file(editor) {
|
/// to the path is already open in the editor, it is used instead.
|
||||||
if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() {
|
fn get_preview<'picker, 'editor>(
|
||||||
// TODO: enable syntax highlighting; blocked by async rendering
|
&'picker mut self,
|
||||||
let doc = Document::open(&path, None, Some(&editor.theme), None).unwrap();
|
path: &Path,
|
||||||
self.preview_cache.insert(path, doc);
|
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 @@ impl<T: 'static> Component for FilePicker<T> {
|
||||||
// |picker | | |
|
// |picker | | |
|
||||||
// | | | |
|
// | | | |
|
||||||
// +---------+ +---------+
|
// +---------+ +---------+
|
||||||
self.calculate_preview(cx.editor);
|
|
||||||
let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
|
let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
|
||||||
let area = inner_rect(area);
|
let area = inner_rect(area);
|
||||||
// -- Render the frame:
|
// -- Render the frame:
|
||||||
// clear area
|
// clear area
|
||||||
let background = cx.editor.theme.get("ui.background");
|
let background = cx.editor.theme.get("ui.background");
|
||||||
|
let text = cx.editor.theme.get("ui.text");
|
||||||
surface.clear_with(area, background);
|
surface.clear_with(area, background);
|
||||||
|
|
||||||
let picker_width = if render_preview {
|
let picker_width = if render_preview {
|
||||||
|
@ -113,17 +190,23 @@ impl<T: 'static> Component for FilePicker<T> {
|
||||||
horizontal: 1,
|
horizontal: 1,
|
||||||
};
|
};
|
||||||
let inner = inner.inner(&margin);
|
let inner = inner.inner(&margin);
|
||||||
|
|
||||||
block.render(preview_area, surface);
|
block.render(preview_area, surface);
|
||||||
|
|
||||||
if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, range)| {
|
if let Some((path, range)) = self.current_file(cx.editor) {
|
||||||
cx.editor
|
let preview = self.get_preview(&path, cx.editor);
|
||||||
.document_by_path(&path)
|
let doc = match preview.document() {
|
||||||
.or_else(|| self.preview_cache.get(&path))
|
Some(doc) => doc,
|
||||||
.zip(Some(range))
|
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
|
// align to middle
|
||||||
let first_line = line
|
let first_line = range
|
||||||
.map(|(start, end)| {
|
.map(|(start, end)| {
|
||||||
let height = end.saturating_sub(start) + 1;
|
let height = end.saturating_sub(start) + 1;
|
||||||
let middle = start + (height.saturating_sub(1) / 2);
|
let middle = start + (height.saturating_sub(1) / 2);
|
||||||
|
@ -150,7 +233,7 @@ impl<T: 'static> Component for FilePicker<T> {
|
||||||
);
|
);
|
||||||
|
|
||||||
// highlight the line
|
// highlight the line
|
||||||
if let Some((start, end)) = line {
|
if let Some((start, end)) = range {
|
||||||
let offset = start.saturating_sub(first_line) as u16;
|
let offset = start.saturating_sub(first_line) as u16;
|
||||||
surface.set_style(
|
surface.set_style(
|
||||||
Rect::new(
|
Rect::new(
|
||||||
|
|
Loading…
Reference in New Issue