@ -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 = rang e
. 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 ) ) = lin e {
if let Some ( ( start , end ) ) = rang e {
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 (