aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src/ui/picker.rs
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src/ui/picker.rs')
-rw-r--r--helix-term/src/ui/picker.rs157
1 files changed, 120 insertions, 37 deletions
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index 341235ee..3e805fac 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -12,7 +12,12 @@ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
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 @@ use helix_view::{
};
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 @@ impl<T> FilePicker<T> {
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 @@ impl<T> FilePicker<T> {
})
}
- 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 @@ impl<T: 'static> Component for FilePicker<T> {
// |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 @@ impl<T: 'static> Component for FilePicker<T> {
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 @@ impl<T: 'static> Component for FilePicker<T> {
);
// 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(
@@ -234,37 +317,28 @@ impl<T> Picker<T> {
}
pub fn score(&mut self) {
- // need to borrow via pattern match otherwise it complains about simultaneous borrow
- let Self {
- ref mut matcher,
- ref mut matches,
- ref filters,
- ref format_fn,
- ..
- } = *self;
-
let pattern = &self.prompt.line;
// reuse the matches allocation
- matches.clear();
- matches.extend(
+ self.matches.clear();
+ self.matches.extend(
self.options
.iter()
.enumerate()
.filter_map(|(index, option)| {
// filter options first before matching
- if !filters.is_empty() {
- filters.binary_search(&index).ok()?;
+ if !self.filters.is_empty() {
+ self.filters.binary_search(&index).ok()?;
}
// TODO: maybe using format_fn isn't the best idea here
- let text = (format_fn)(option);
+ let text = (self.format_fn)(option);
// TODO: using fuzzy_indices could give us the char idx for match highlighting
- matcher
+ self.matcher
.fuzzy_match(&text, pattern)
.map(|score| (index, score))
}),
);
- matches.sort_unstable_by_key(|(_, score)| -score);
+ self.matches.sort_unstable_by_key(|(_, score)| -score);
// reset cursor position
self.cursor = 0;
@@ -338,6 +412,10 @@ impl<T: 'static> Component for Picker<T> {
..
}
| KeyEvent {
+ code: KeyCode::Char('k'),
+ modifiers: KeyModifiers::CONTROL,
+ }
+ | KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
} => {
@@ -351,6 +429,10 @@ impl<T: 'static> Component for Picker<T> {
code: KeyCode::Tab, ..
}
| KeyEvent {
+ code: KeyCode::Char('j'),
+ modifiers: KeyModifiers::CONTROL,
+ }
+ | KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
} => {
@@ -375,7 +457,7 @@ impl<T: 'static> Component for Picker<T> {
return close_fn;
}
KeyEvent {
- code: KeyCode::Char('h'),
+ code: KeyCode::Char('s'),
modifiers: KeyModifiers::CONTROL,
} => {
if let Some(option) = self.selection() {
@@ -485,6 +567,7 @@ impl<T: 'static> Component for Picker<T> {
text_style
},
true,
+ true,
);
}
}