1 use {
2     super::*,
3     crate::{
4         app::{AppContext, LineNumber},
5         command::{ScrollCommand, move_sel},
6         display::{Screen, W},
7         errors::*,
8         pattern::{InputPattern, NameMatch},
9         skin::PanelSkin,
10         task_sync::Dam,
11     },
12     crossterm::{
13         cursor,
14         style::{Color, Print, SetBackgroundColor, SetForegroundColor},
15         QueueableCommand,
16     },
17     memmap::Mmap,
18     once_cell::sync::Lazy,
19     std::{
20         fs::File,
21         io::{BufRead, BufReader},
22         path::{Path, PathBuf},
23         str,
24     },
25     syntect::highlighting::Style,
26     termimad::{Area, CropWriter, SPACE_FILLING},
27 };
28 
29 /// a homogeneously colored piece of a line
30 #[derive(Debug)]
31 pub struct Region {
32     pub fg: Color,
33     pub string: String,
34 }
35 
36 /// when the file is bigger, we don't style it and we don't keep
37 /// it in memory: we just keep the offsets of the lines in the
38 /// file.
39 const MAX_SIZE_FOR_STYLING: u64 = 2_000_000;
40 
41 impl Region {
from_syntect(region: &(Style, &str)) -> Self42     pub fn from_syntect(region: &(Style, &str)) -> Self {
43         let fg = Color::Rgb {
44             r: region.0.foreground.r,
45             g: region.0.foreground.g,
46             b: region.0.foreground.b,
47         };
48         let string = region.1.to_string();
49         Self { fg, string }
50     }
51 }
52 
53 #[derive(Debug)]
54 pub struct Line {
55     pub number: LineNumber,   // starting at 1
56     pub start: usize,         // offset in the file, in bytes
57     pub len: usize,           // len in bytes
58     pub regions: Vec<Region>, // not always computed
59     pub name_match: Option<NameMatch>,
60 }
61 
62 pub struct SyntacticView {
63     pub path: PathBuf,
64     pub pattern: InputPattern,
65     lines: Vec<Line>,
66     scroll: usize,
67     page_height: usize,
68     selection_idx: Option<usize>, // index in lines of the selection, if any
69     total_lines_count: usize,     // including lines not filtered out
70 }
71 
72 impl SyntacticView {
73 
74     /// return a prepared text view with syntax coloring if possible.
75     /// May return Ok(None) only when a pattern is given and there
76     /// was an event before the end of filtering.
new( path: &Path, pattern: InputPattern, dam: &mut Dam, con: &AppContext, ) -> Result<Option<Self>, ProgramError>77     pub fn new(
78         path: &Path,
79         pattern: InputPattern,
80         dam: &mut Dam,
81         con: &AppContext,
82     ) -> Result<Option<Self>, ProgramError> {
83         let mut sv = Self {
84             path: path.to_path_buf(),
85             pattern,
86             lines: Vec::new(),
87             scroll: 0,
88             page_height: 0,
89             selection_idx: None,
90             total_lines_count: 0,
91         };
92         if sv.read_lines(dam, con)? {
93             sv.select_first();
94             Ok(Some(sv))
95         } else {
96             Ok(None)
97         }
98     }
99 
100     /// return true when there was no interruption
read_lines( &mut self, dam: &mut Dam, con: &AppContext, ) -> Result<bool, ProgramError>101     fn read_lines(
102         &mut self,
103         dam: &mut Dam,
104         con: &AppContext,
105     ) -> Result<bool, ProgramError> {
106         let f = File::open(&self.path)?;
107         {
108             // if we detect the file isn't mappable, we'll
109             // let the ZeroLenFilePreview try to read it
110             let mmap = unsafe { Mmap::map(&f) };
111             if mmap.is_err() {
112                 return Err(ProgramError::UnmappableFile);
113             }
114         }
115         let md = f.metadata()?;
116         if md.len() == 0 {
117             return Err(ProgramError::ZeroLenFile);
118         }
119         let with_style = md.len() < MAX_SIZE_FOR_STYLING;
120         let mut reader = BufReader::new(f);
121         self.lines.clear();
122         let mut line = String::new();
123         self.total_lines_count = 0;
124         let mut offset = 0;
125         let mut number = 0;
126         static SYNTAXER: Lazy<Syntaxer> = Lazy::new(Syntaxer::default);
127         let mut highlighter = if with_style {
128             SYNTAXER.highlighter_for(&self.path, con)
129         } else {
130             None
131         };
132         let pattern = &self.pattern.pattern;
133         while reader.read_line(&mut line)? > 0 {
134             number += 1;
135             self.total_lines_count += 1;
136             let start = offset;
137             offset += line.len();
138             while line.ends_with('\n') || line.ends_with('\r') {
139                 line.pop();
140             }
141             if pattern.is_empty() || pattern.score_of_string(&line).is_some() {
142                 let name_match = pattern.search_string(&line);
143                 let regions = if let Some(highlighter) = highlighter.as_mut() {
144                     highlighter
145                         .highlight(&line, &SYNTAXER.syntax_set)
146                         .iter()
147                         .map(|r| Region::from_syntect(r))
148                         .collect()
149                 } else {
150                     Vec::new()
151                 };
152                 self.lines.push(Line {
153                     regions,
154                     start,
155                     len: line.len(),
156                     name_match,
157                     number,
158                 });
159             }
160             line.clear();
161             if dam.has_event() {
162                 info!("event interrupted preview filtering");
163                 return Ok(false);
164             }
165         }
166         Ok(true)
167     }
168 
169     /// (count of lines which can be seen when scrolling,
170     /// total count including filtered ones)
line_counts(&self) -> (usize, usize)171     pub fn line_counts(&self) -> (usize, usize) {
172         (self.lines.len(), self.total_lines_count)
173     }
174 
ensure_selection_is_visible(&mut self)175     fn ensure_selection_is_visible(&mut self) {
176         if let Some(idx) = self.selection_idx {
177             let padding = self.padding();
178             if idx < self.scroll + padding || idx + padding > self.scroll + self.page_height {
179                 if idx <= padding {
180                     self.scroll = 0;
181                 } else if idx + padding > self.lines.len() {
182                     self.scroll = self.lines.len() - self.page_height;
183                 } else if idx < self.scroll + self.page_height / 2 {
184                     self.scroll = idx - padding;
185                 } else {
186                     self.scroll = idx + padding - self.page_height;
187                 }
188             }
189         }
190     }
191 
padding(&self) -> usize192     fn padding(&self) -> usize {
193         (self.page_height / 4).min(4)
194     }
195 
get_selected_line(&self) -> Option<String>196     pub fn get_selected_line(&self) -> Option<String> {
197         self.selection_idx
198             .and_then(|idx| self.lines.get(idx))
199             .and_then(|line| {
200                 File::open(&self.path)
201                     .and_then(|file| unsafe { Mmap::map(&file) })
202                     .ok()
203                     .filter(|mmap| mmap.len() >= line.start + line.len)
204                     .and_then(|mmap| {
205                         String::from_utf8(
206                             (&mmap[line.start..line.start + line.len]).to_vec(),
207                         ).ok()
208                     })
209             })
210     }
211 
get_selected_line_number(&self) -> Option<LineNumber>212     pub fn get_selected_line_number(&self) -> Option<LineNumber> {
213         self.selection_idx
214             .map(|idx| self.lines[idx].number)
215     }
unselect(&mut self)216     pub fn unselect(&mut self) {
217         self.selection_idx = None;
218     }
try_select_y(&mut self, y: u16) -> bool219     pub fn try_select_y(&mut self, y: u16) -> bool {
220         let idx = y as usize + self.scroll;
221         if idx < self.lines.len() {
222             self.selection_idx = Some(idx);
223             true
224         } else {
225             false
226         }
227     }
228 
select_first(&mut self)229     pub fn select_first(&mut self) {
230         if !self.lines.is_empty() {
231             self.selection_idx = Some(0);
232             self.scroll = 0;
233         }
234     }
select_last(&mut self)235     pub fn select_last(&mut self) {
236         self.selection_idx = Some(self.lines.len() - 1);
237         if self.page_height < self.lines.len() {
238             self.scroll = self.lines.len() - self.page_height;
239         }
240     }
241 
try_select_line_number(&mut self, number: LineNumber) -> bool242     pub fn try_select_line_number(&mut self, number: LineNumber) -> bool {
243         // this could obviously be optimized
244         for (idx, line) in self.lines.iter().enumerate() {
245             if line.number == number {
246                 self.selection_idx = Some(idx);
247                 self.ensure_selection_is_visible();
248                 return true;
249             }
250         }
251         false
252     }
253 
move_selection(&mut self, dy: i32, cycle: bool)254     pub fn move_selection(&mut self, dy: i32, cycle: bool) {
255         if let Some(idx) = self.selection_idx {
256             self.selection_idx = Some(move_sel(idx, self.lines.len(), dy, cycle));
257         } else if !self.lines.is_empty() {
258             self.selection_idx = Some(0)
259         }
260         self.ensure_selection_is_visible();
261     }
262 
try_scroll( &mut self, cmd: ScrollCommand, ) -> bool263     pub fn try_scroll(
264         &mut self,
265         cmd: ScrollCommand,
266     ) -> bool {
267         let old_scroll = self.scroll;
268         self.scroll = cmd.apply(self.scroll, self.lines.len(), self.page_height);
269         if let Some(idx) = self.selection_idx {
270             if self.scroll == old_scroll {
271                 let old_selection = self.selection_idx;
272                 if cmd.is_up() {
273                     self.selection_idx = Some(0);
274                 } else {
275                     self.selection_idx = Some(self.lines.len() - 1);
276                 }
277                 return self.selection_idx == old_selection;
278             } else  if idx >= old_scroll && idx < old_scroll + self.page_height {
279                 if idx + self.scroll < old_scroll {
280                     self.selection_idx = Some(0);
281                 } else if idx + self.scroll - old_scroll >= self.lines.len() {
282                     self.selection_idx = Some(self.lines.len() - 1);
283                 } else {
284                     self.selection_idx = Some(idx + self.scroll - old_scroll);
285                 }
286             }
287         }
288         self.scroll != old_scroll
289     }
290 
display( &mut self, w: &mut W, _screen: Screen, panel_skin: &PanelSkin, area: &Area, con: &AppContext, ) -> Result<(), ProgramError>291     pub fn display(
292         &mut self,
293         w: &mut W,
294         _screen: Screen,
295         panel_skin: &PanelSkin,
296         area: &Area,
297         con: &AppContext,
298     ) -> Result<(), ProgramError> {
299         if area.height as usize != self.page_height {
300             self.page_height = area.height as usize;
301             self.ensure_selection_is_visible();
302         }
303         let max_number_len = self.lines.last().map_or(0, |l|l.number).to_string().len();
304         let show_line_number = area.width > 55 || ( self.pattern.is_some() && area.width > 8 );
305         let line_count = area.height as usize;
306         let styles = &panel_skin.styles;
307         let normal_fg  = styles.preview.get_fg()
308             .or_else(|| styles.default.get_fg())
309             .unwrap_or(Color::AnsiValue(252));
310         let normal_bg = styles.preview.get_bg()
311             .or_else(|| styles.default.get_bg())
312             .unwrap_or(Color::AnsiValue(238));
313         let selection_bg = styles.selected_line.get_bg()
314             .unwrap_or(Color::AnsiValue(240));
315         let match_bg = styles.preview_match.get_bg().unwrap_or(Color::AnsiValue(28));
316         let code_width = area.width as usize - 1; // 1 char left for scrollbar
317         let scrollbar = area.scrollbar(self.scroll, self.lines.len());
318         let scrollbar_fg = styles.scrollbar_thumb.get_fg()
319             .or_else(|| styles.preview.get_fg())
320             .unwrap_or(Color::White);
321         for y in 0..line_count {
322             w.queue(cursor::MoveTo(area.left, y as u16 + area.top))?;
323             let mut cw = CropWriter::new(w, code_width);
324             let line_idx = self.scroll as usize + y;
325             let selected = self.selection_idx == Some(line_idx);
326             let bg = if selected { selection_bg } else { normal_bg };
327             let mut op_mmap: Option<Mmap> = None;
328             if let Some(line) = self.lines.get(line_idx) {
329                 let mut regions = &line.regions;
330                 let regions_ur;
331                 if regions.is_empty() && line.len > 0 {
332                     if op_mmap.is_none() {
333                         let file = File::open(&self.path)?;
334                         let mmap = unsafe { Mmap::map(&file)? };
335                         op_mmap = Some(mmap);
336                     }
337                     if op_mmap.as_ref().unwrap().len() < line.start + line.len {
338                         warn!("file truncated since parsing");
339                     } else {
340                         // an UTF8 error can only happen if file modified during display
341                         let string = String::from_utf8(
342                             // we copy the memmap slice, as it's not immutable
343                             (&op_mmap.unwrap()[line.start..line.start + line.len]).to_vec(),
344                         )
345                         .unwrap_or_else(|_| "Bad UTF8".to_string());
346                         regions_ur = vec![Region {
347                             fg: normal_fg,
348                             string,
349                         }];
350                         regions = &regions_ur;
351                     }
352                 }
353                 cw.w.queue(SetBackgroundColor(bg))?;
354                 if show_line_number {
355                     cw.queue_g_string(
356                         &styles.preview_line_number,
357                         format!(" {:w$} ", line.number, w = max_number_len),
358                     )?;
359                 } else {
360                     cw.queue_unstyled_str(" ")?;
361                 }
362                 cw.w.queue(SetBackgroundColor(bg))?;
363                 if con.show_selection_mark {
364                     cw.queue_unstyled_char(if selected { '▶' } else { ' ' })?;
365                 }
366                 if let Some(nm) = &line.name_match {
367                     let mut dec = 0;
368                     let pos = &nm.pos;
369                     let mut pos_idx: usize = 0;
370                     for content in regions {
371                         let s = &content.string;
372                         cw.w.queue(SetForegroundColor(content.fg))?;
373                         if pos_idx < pos.len() {
374                             for (cand_idx, cand_char) in s.chars().enumerate() {
375                                 if pos_idx < pos.len() && pos[pos_idx] == cand_idx + dec {
376                                     cw.w.queue(SetBackgroundColor(match_bg))?;
377                                     cw.queue_unstyled_char(cand_char)?;
378                                     cw.w.queue(SetBackgroundColor(bg))?;
379                                     pos_idx += 1;
380                                 } else {
381                                     cw.queue_unstyled_char(cand_char)?;
382                                 }
383                             }
384                             dec += s.chars().count();
385                         } else {
386                             cw.queue_unstyled_str(s)?;
387                         }
388                     }
389                 } else {
390                     for content in regions {
391                         cw.w.queue(SetForegroundColor(content.fg))?;
392                         cw.queue_unstyled_str(&content.string)?;
393                     }
394                 }
395             }
396             cw.fill(
397                 if selected { &styles.selected_line } else { &styles.preview },
398                 &SPACE_FILLING,
399             )?;
400             w.queue(SetBackgroundColor(bg))?;
401             if is_thumb(y, scrollbar) {
402                 w.queue(SetForegroundColor(scrollbar_fg))?;
403                 w.queue(Print('▐'))?;
404             } else {
405                 w.queue(Print(' '))?;
406             }
407         }
408         Ok(())
409     }
410 
display_info( &mut self, w: &mut W, _screen: Screen, panel_skin: &PanelSkin, area: &Area, ) -> Result<(), ProgramError>411     pub fn display_info(
412         &mut self,
413         w: &mut W,
414         _screen: Screen,
415         panel_skin: &PanelSkin,
416         area: &Area,
417     ) -> Result<(), ProgramError> {
418         let width = area.width as usize;
419         let mut s = if self.pattern.is_some() {
420             format!("{}/{}", self.lines.len(), self.total_lines_count)
421         } else {
422             format!("{}", self.total_lines_count)
423         };
424         if s.len() > width {
425             return Ok(());
426         }
427         if s.len() + "lines: ".len() < width {
428             s = format!("lines: {}", s);
429         }
430         w.queue(cursor::MoveTo(
431             area.left + area.width - s.len() as u16,
432             area.top,
433         ))?;
434         panel_skin.styles.default.queue(w, s)?;
435         Ok(())
436     }
437 }
438 
is_thumb(y: usize, scrollbar: Option<(u16, u16)>) -> bool439 fn is_thumb(y: usize, scrollbar: Option<(u16, u16)>) -> bool {
440     scrollbar.map_or(false, |(sctop, scbottom)| {
441         let y = y as u16;
442         sctop <= y && y <= scbottom
443     })
444 }
445 
446