@@ -12,7 +12,12 @@ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
12use fuzzy_matcher::FuzzyMatcher;
12use fuzzy_matcher::FuzzyMatcher;
13use tui::widgets::Widget;
13use tui::widgets::Widget;
15−use std::{borrow::Cow, collections::HashMap, path::PathBuf};
17+ collections::HashMap,
19+ path::{Path, PathBuf},
17use crate::ui::{Prompt, PromptEvent};
22use crate::ui::{Prompt, PromptEvent};
18use helix_core::Position;
23use helix_core::Position;
@@ -23,18 +28,58 @@ use helix_view::{
25pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80;
30pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80;
31+/// Biggest file size to preview in bytes
32+pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
27−/// File path and line number (used to align and highlight a line)
34+/// File path and range of lines (used to align and highlight lines)
28type FileLocation = (PathBuf, Option<(usize, usize)>);
35type FileLocation = (PathBuf, Option<(usize, usize)>);
30pub struct FilePicker<T> {
37pub struct FilePicker<T> {
31 picker: Picker<T>,
38 picker: Picker<T>,
32 /// Caches paths to documents
39 /// Caches paths to documents
33− preview_cache: HashMap<PathBuf, Document>,
40+ preview_cache: HashMap<PathBuf, CachedPreview>,
41+ read_buffer: Vec<u8>,
34 /// Given an item in the picker, return the file path and line number to display.
42 /// Given an item in the picker, return the file path and line number to display.
35 file_fn: Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>,
43 file_fn: Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>,
46+pub enum CachedPreview {
53+// We don't store this enum in the cache so as to avoid lifetime constraints
54+// from borrowing a document already opened in the editor.
55+pub enum Preview<'picker, 'editor> {
56+ Cached(&'picker CachedPreview),
57+ EditorDocument(&'editor Document),
60+impl Preview<'_, '_> {
61+ fn document(&self) -> Option<&Document> {
63+ Preview::EditorDocument(doc) => Some(doc),
64+ Preview::Cached(CachedPreview::Document(doc)) => Some(doc),
69+ /// Alternate text to show for the preview.
70+ fn placeholder(&self) -> &str {
72+ Self::EditorDocument(_) => "<File preview>",
73+ Self::Cached(preview) => match preview {
74+ CachedPreview::Document(_) => "<File preview>",
75+ CachedPreview::Binary => "<Binary file>",
76+ CachedPreview::LargeFile => "<File too large to preview>",
77+ CachedPreview::NotFound => "<File not found>",
38impl<T> FilePicker<T> {
83impl<T> FilePicker<T> {
39 pub fn new(
84 pub fn new(
40 options: Vec<T>,
85 options: Vec<T>,
@@ -45,6 +90,7 @@ impl<T> FilePicker<T> {
46 picker: Picker::new(false, options, format_fn, callback_fn),
91 picker: Picker::new(false, options, format_fn, callback_fn),
47 preview_cache: HashMap::new(),
92 preview_cache: HashMap::new(),
93+ read_buffer: Vec::with_capacity(1024),
48 file_fn: Box::new(preview_fn),
94 file_fn: Box::new(preview_fn),
@@ -60,14 +106,45 @@ impl<T> FilePicker<T> {
109+ /// Get (cached) preview for a given path. If a document corresponding
110+ /// to the path is already open in the editor, it is used instead.
111+ fn get_preview<'picker, 'editor>(
63− fn calculate_preview(&mut self, editor: &Editor) {
114+ editor: &'editor Editor,
64− if let Some((path, _line)) = self.current_file(editor) {
115+ ) -> Preview<'picker, 'editor> {
65− if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() {
116+ if let Some(doc) = editor.document_by_path(path) {
66− // TODO: enable syntax highlighting; blocked by async rendering
117+ return Preview::EditorDocument(doc);
67− let doc = Document::open(&path, None, Some(&editor.theme), None).unwrap();
68− self.preview_cache.insert(path, doc);
120+ if self.preview_cache.contains_key(path) {
121+ return Preview::Cached(&self.preview_cache[path]);
124+ let data = std::fs::File::open(path).and_then(|file| {
125+ let metadata = file.metadata()?;
126+ // Read up to 1kb to detect the content type
127+ let n = file.take(1024).read_to_end(&mut self.read_buffer)?;
128+ let content_type = content_inspector::inspect(&self.read_buffer[..n]);
129+ self.read_buffer.clear();
130+ Ok((metadata, content_type))
134+ |(metadata, content_type)| match (metadata.len(), content_type) {
135+ (_, content_inspector::ContentType::BINARY) => CachedPreview::Binary,
136+ (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => CachedPreview::LargeFile,
138+ // TODO: enable syntax highlighting; blocked by async rendering
139+ Document::open(path, None, Some(&editor.theme), None)
140+ .map(CachedPreview::Document)
141+ .unwrap_or(CachedPreview::NotFound)
145+ .unwrap_or(CachedPreview::NotFound);
146+ self.preview_cache.insert(path.to_owned(), preview);
147+ Preview::Cached(&self.preview_cache[path])
@@ -79,12 +156,12 @@ impl<T: 'static> Component for FilePicker<T> {
79 // |picker | | |
156 // |picker | | |
80 // | | | |
157 // | | | |
81 // +---------+ +---------+
158 // +---------+ +---------+
82− self.calculate_preview(cx.editor);
83 let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
159 let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
84 let area = inner_rect(area);
160 let area = inner_rect(area);
85 // -- Render the frame:
161 // -- Render the frame:
86 // clear area
162 // clear area
87 let background = cx.editor.theme.get("ui.background");
163 let background = cx.editor.theme.get("ui.background");
164+ let text = cx.editor.theme.get("ui.text");
88 surface.clear_with(area, background);
165 surface.clear_with(area, background);
90 let picker_width = if render_preview {
167 let picker_width = if render_preview {
@@ -113,17 +190,23 @@ impl<T: 'static> Component for FilePicker<T> {
113 horizontal: 1,
190 horizontal: 1,
115 let inner = inner.inner(&margin);
192 let inner = inner.inner(&margin);
117 block.render(preview_area, surface);
193 block.render(preview_area, surface);
119− if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, range)| {
195+ if let Some((path, range)) = self.current_file(cx.editor) {
120− cx.editor
196+ let preview = self.get_preview(&path, cx.editor);
121− .document_by_path(&path)
197+ let doc = match preview.document() {
122− .or_else(|| self.preview_cache.get(&path))
198+ Some(doc) => doc,
123− .zip(Some(range))
199+ None => {
200+ let alt_text = preview.placeholder();
201+ let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2;
202+ let y = inner.y + inner.height / 2;
203+ surface.set_stringn(x, y, alt_text, inner.width as usize, text);
125 // align to middle
208 // align to middle
126− let first_line = line
209+ let first_line = range
127 .map(|(start, end)| {
210 .map(|(start, end)| {
128 let height = end.saturating_sub(start) + 1;
211 let height = end.saturating_sub(start) + 1;
129 let middle = start + (height.saturating_sub(1) / 2);
212 let middle = start + (height.saturating_sub(1) / 2);
@@ -150,7 +233,7 @@ impl<T: 'static> Component for FilePicker<T> {
152 // highlight the line
235 // highlight the line
153− if let Some((start, end)) = line {
236+ if let Some((start, end)) = range {
154 let offset = start.saturating_sub(first_line) as u16;
237 let offset = start.saturating_sub(first_line) as u16;
155 surface.set_style(
238 surface.set_style(
156 Rect::new(
239 Rect::new(