@@ -12,7 +12,12 @@ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
12
12
use fuzzy_matcher:: FuzzyMatcher ;
13
13
use tui:: widgets:: Widget ;
14
14
15
- use std:: { borrow:: Cow , collections:: HashMap , path:: PathBuf } ;
15
+ use std:: {
16
+ borrow:: Cow ,
17
+ collections:: HashMap ,
18
+ io:: Read ,
19
+ path:: { Path , PathBuf } ,
20
+ } ;
16
21
17
22
use crate :: ui:: { Prompt , PromptEvent } ;
18
23
use helix_core:: Position ;
@@ -23,18 +28,58 @@ use helix_view::{
23
28
} ;
24
29
25
30
pub 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 ;
26
33
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 )
28
35
type FileLocation = ( PathBuf , Option < ( usize , usize ) > ) ;
29
36
30
37
pub struct FilePicker < T > {
31
38
picker : Picker < T > ,
32
39
/// Caches paths to documents
33
- preview_cache : HashMap < PathBuf , Document > ,
40
+ preview_cache : HashMap < PathBuf , CachedPreview > ,
41
+ read_buffer : Vec < u8 > ,
34
42
/// Given an item in the picker, return the file path and line number to display.
35
43
file_fn : Box < dyn Fn ( & Editor , & T ) -> Option < FileLocation > > ,
36
44
}
37
45
46
+ pub enum CachedPreview {
47
+ Document ( Document ) ,
48
+ Binary ,
49
+ LargeFile ,
50
+ NotFound ,
51
+ }
52
+
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 ) ,
58
+ }
59
+
60
+ impl Preview < ' _ , ' _ > {
61
+ fn document ( & self ) -> Option < & Document > {
62
+ match self {
63
+ Preview :: EditorDocument ( doc) => Some ( doc) ,
64
+ Preview :: Cached ( CachedPreview :: Document ( doc) ) => Some ( doc) ,
65
+ _ => None ,
66
+ }
67
+ }
68
+
69
+ /// Alternate text to show for the preview.
70
+ fn placeholder ( & self ) -> & str {
71
+ match * self {
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>" ,
78
+ } ,
79
+ }
80
+ }
81
+ }
82
+
38
83
impl < T > FilePicker < T > {
39
84
pub fn new (
40
85
options : Vec < T > ,
@@ -45,6 +90,7 @@ impl<T> FilePicker<T> {
45
90
Self {
46
91
picker : Picker :: new ( false , options, format_fn, callback_fn) ,
47
92
preview_cache : HashMap :: new ( ) ,
93
+ read_buffer : Vec :: with_capacity ( 1024 ) ,
48
94
file_fn : Box :: new ( preview_fn) ,
49
95
}
50
96
}
@@ -60,14 +106,45 @@ impl<T> FilePicker<T> {
60
106
} )
61
107
}
62
108
63
- fn calculate_preview ( & mut self , editor : & Editor ) {
64
- if let Some ( ( path, _line) ) = self . current_file ( editor) {
65
- if !self . preview_cache . contains_key ( & path) && editor. document_by_path ( & path) . is_none ( ) {
66
- // TODO: enable syntax highlighting; blocked by async rendering
67
- let doc = Document :: open ( & path, None , Some ( & editor. theme ) , None ) . unwrap ( ) ;
68
- self . preview_cache . insert ( path, doc) ;
69
- }
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 > (
112
+ & ' picker mut self ,
113
+ path : & Path ,
114
+ editor : & ' editor Editor ,
115
+ ) -> Preview < ' picker , ' editor > {
116
+ if let Some ( doc) = editor. document_by_path ( path) {
117
+ return Preview :: EditorDocument ( doc) ;
118
+ }
119
+
120
+ if self . preview_cache . contains_key ( path) {
121
+ return Preview :: Cached ( & self . preview_cache [ path] ) ;
70
122
}
123
+
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) )
131
+ } ) ;
132
+ let preview = data
133
+ . map (
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 ,
137
+ _ => {
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 )
142
+ }
143
+ } ,
144
+ )
145
+ . unwrap_or ( CachedPreview :: NotFound ) ;
146
+ self . preview_cache . insert ( path. to_owned ( ) , preview) ;
147
+ Preview :: Cached ( & self . preview_cache [ path] )
71
148
}
72
149
}
73
150
@@ -79,12 +156,12 @@ impl<T: 'static> Component for FilePicker<T> {
79
156
// |picker | | |
80
157
// | | | |
81
158
// +---------+ +---------+
82
- self . calculate_preview ( cx. editor ) ;
83
159
let render_preview = area. width > MIN_SCREEN_WIDTH_FOR_PREVIEW ;
84
160
let area = inner_rect ( area) ;
85
161
// -- Render the frame:
86
162
// clear area
87
163
let background = cx. editor . theme . get ( "ui.background" ) ;
164
+ let text = cx. editor . theme . get ( "ui.text" ) ;
88
165
surface. clear_with ( area, background) ;
89
166
90
167
let picker_width = if render_preview {
@@ -113,17 +190,23 @@ impl<T: 'static> Component for FilePicker<T> {
113
190
horizontal : 1 ,
114
191
} ;
115
192
let inner = inner. inner ( & margin) ;
116
-
117
193
block. render ( preview_area, surface) ;
118
194
119
- if let Some ( ( doc, line) ) = self . current_file ( cx. editor ) . and_then ( |( path, range) | {
120
- cx. editor
121
- . document_by_path ( & path)
122
- . or_else ( || self . preview_cache . get ( & path) )
123
- . zip ( Some ( range) )
124
- } ) {
195
+ if let Some ( ( path, range) ) = self . current_file ( cx. editor ) {
196
+ let preview = self . get_preview ( & path, cx. editor ) ;
197
+ let doc = match preview. document ( ) {
198
+ Some ( doc) => doc,
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) ;
204
+ return ;
205
+ }
206
+ } ;
207
+
125
208
// align to middle
126
- let first_line = line
209
+ let first_line = range
127
210
. map ( |( start, end) | {
128
211
let height = end. saturating_sub ( start) + 1 ;
129
212
let middle = start + ( height. saturating_sub ( 1 ) / 2 ) ;
@@ -150,7 +233,7 @@ impl<T: 'static> Component for FilePicker<T> {
150
233
) ;
151
234
152
235
// highlight the line
153
- if let Some ( ( start, end) ) = line {
236
+ if let Some ( ( start, end) ) = range {
154
237
let offset = start. saturating_sub ( first_line) as u16 ;
155
238
surface. set_style (
156
239
Rect :: new (
0 commit comments