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 = ®ions_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