From af799374950c7a9ba709924e53005640032faa28 Mon Sep 17 00:00:00 2001 From: alexpasmantier Date: Thu, 10 Oct 2024 22:58:13 +0200 Subject: [PATCH] general code improvements --- src/components.rs | 1 + src/components/previewers/env.rs | 2 +- src/components/previewers/files.rs | 41 ++--- src/components/television.rs | 287 ++++++----------------------- src/components/ui.rs | 160 ++++++++++++++++ src/components/utils/files.rs | 31 ++-- src/components/utils/strings.rs | 26 ++- 7 files changed, 274 insertions(+), 274 deletions(-) create mode 100644 src/components/ui.rs diff --git a/src/components.rs b/src/components.rs index 5cf1fdc..b7acc08 100644 --- a/src/components.rs +++ b/src/components.rs @@ -13,6 +13,7 @@ mod fuzzy; mod input; mod previewers; pub mod television; +mod ui; mod utils; pub use finders::Entry; diff --git a/src/components/previewers/env.rs b/src/components/previewers/env.rs index 7dbc387..e43aa28 100644 --- a/src/components/previewers/env.rs +++ b/src/components/previewers/env.rs @@ -24,7 +24,7 @@ impl EnvVarPreviewer { title: entry.name.clone(), content: if let Some(preview) = &entry.preview { PreviewContent::PlainTextWrapped(maybe_add_newline_after_colon( - &preview, + preview, &entry.name, )) } else { diff --git a/src/components/previewers/files.rs b/src/components/previewers/files.rs index 4fcc0c9..8942c36 100644 --- a/src/components/previewers/files.rs +++ b/src/components/previewers/files.rs @@ -1,5 +1,5 @@ use color_eyre::Result; -use image::{ImageReader, Rgb}; +use image::Rgb; use ratatui_image::picker::Picker; use std::fs::File; use std::io::{BufRead, BufReader, Read}; @@ -107,7 +107,7 @@ impl FilePreviewer { fn get_file_type(&self, path: &Path) -> FileType { debug!("Getting file type for {:?}", path); - let mut file_type = match infer::get_from_path(&path) { + let mut file_type = match infer::get_from_path(path) { Ok(Some(t)) => { let mime_type = t.mime_type(); if mime_type.contains("image") { @@ -123,15 +123,13 @@ impl FilePreviewer { // if the file type is unknown, try to determine it from the extension or the content if matches!(file_type, FileType::Unknown) { - if is_known_text_extension(&path) { + if is_known_text_extension(path) { file_type = FileType::Text; - } else { - if let Ok(mut f) = File::open(&path) { - let mut buffer = [0u8; 256]; - if let Ok(bytes_read) = f.read(&mut buffer) { - if bytes_read > 0 && is_valid_utf8(&buffer) { - file_type = FileType::Text; - } + } else if let Ok(mut f) = File::open(path) { + let mut buffer = [0u8; 256]; + if let Ok(bytes_read) = f.read(&mut buffer) { + if bytes_read > 0 && is_valid_utf8(&buffer) { + file_type = FileType::Text; } } } @@ -167,7 +165,8 @@ impl FilePreviewer { debug!("Computing preview for {:?}", entry.name); match self.get_file_type(&path_buf) { FileType::Text => { - let preview = match File::open(&path_buf) { + + match File::open(&path_buf) { Ok(file) => { // insert a non-highlighted version of the preview into the cache let reader = BufReader::new(file); @@ -175,13 +174,10 @@ impl FilePreviewer { self.cache_preview(entry.name.clone(), preview.clone()) .await; - match preview.content { - PreviewContent::PlainText(ref lines) => { - // compute the highlighted version in the background - self.compute_highlighted_text_preview(entry, lines.to_vec()) - .await; - } - _ => {} + if let PreviewContent::PlainText(ref lines) = preview.content { + // compute the highlighted version in the background + self.compute_highlighted_text_preview(entry, lines.to_vec()) + .await; } preview } @@ -191,8 +187,7 @@ impl FilePreviewer { self.cache_preview(entry.name.clone(), p.clone()).await; p } - }; - preview + } } FileType::Image => { debug!("Previewing image file: {:?}", entry.name); @@ -202,21 +197,21 @@ impl FilePreviewer { .await; // compute the image preview in the background //self.compute_image_preview(entry).await; - return preview; + preview } FileType::Other => { debug!("Previewing other file: {:?}", entry.name); let preview = not_supported(&entry.name); self.cache_preview(entry.name.clone(), preview.clone()) .await; - return preview; + preview } FileType::Unknown => { debug!("Unknown file type: {:?}", entry.name); let preview = not_supported(&entry.name); self.cache_preview(entry.name.clone(), preview.clone()) .await; - return preview; + preview } } } diff --git a/src/components/television.rs b/src/components/television.rs index 09538f0..66812f3 100644 --- a/src/components/television.rs +++ b/src/components/television.rs @@ -2,19 +2,18 @@ use color_eyre::Result; use futures::executor::block_on; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style, Styled, Stylize}, + style::{Color, Modifier, Style, Stylize}, text::{Line, Span, Text}, widgets::{ block::{Position, Title}, - Block, BorderType, Borders, List, ListDirection, ListState, Padding, Paragraph, Wrap, + Block, BorderType, Borders, ListState, Padding, Paragraph, Wrap, }, Frame, }; use ratatui_image::StatefulImage; -use std::{collections::HashMap, str::FromStr, sync::Arc}; +use std::{collections::HashMap, sync::Arc}; use syntect; use syntect::highlighting::Color as SyntectColor; -use tracing::info; use tokio::sync::mpsc::UnboundedSender; @@ -25,7 +24,7 @@ use crate::components::{ previewers::{ Preview, PreviewContent, Previewer, FILE_TOO_LARGE_MSG, PREVIEW_NOT_SUPPORTED_MSG, }, - utils::ui::centered_rect, + ui::{build_results_list, create_layout, get_border_style}, Component, }; use crate::{action::Action, config::Config}; @@ -91,7 +90,7 @@ impl Television { pub fn get_selected_entry(&self) -> Option { self.picker_state .selected() - .and_then(|i| self.channel.get_result(i as u32)) + .and_then(|i| self.channel.get_result(u32::try_from(i).unwrap())) } pub fn select_prev_entry(&mut self) { @@ -115,7 +114,7 @@ impl Television { .selected() .unwrap_or(0) .min(self.results_area_height as usize - 3), - )) + )); } else { self.relative_picker_state.select(Some( (self.relative_picker_state.selected().unwrap_or(0) + 1) @@ -159,26 +158,19 @@ impl Television { if self.preview_scroll.is_none() { self.preview_scroll = Some(0); } - match self.preview_scroll { - Some(scroll) => { - self.preview_scroll = Some( - (scroll + offset).min( - (self.current_preview_total_lines as isize - - (2 * self.preview_pane_height / 3) as isize) - .max(0) as u16, - ), - ); - } - None => {} + if let Some(scroll) = self.preview_scroll { + self.preview_scroll = Some( + (scroll + offset).min( + self.current_preview_total_lines + .saturating_sub(2 * self.preview_pane_height / 3), + ), + ); } } pub fn scroll_preview_up(&mut self, offset: u16) { - match self.preview_scroll { - Some(scroll) => { - self.preview_scroll = Some(scroll.saturating_sub(offset)); - } - None => {} + if let Some(scroll) = self.preview_scroll { + self.preview_scroll = Some(scroll.saturating_sub(offset)); } } @@ -215,11 +207,8 @@ impl Television { /// │ Search x ││ │ /// └───────────────────┘└─────────────┘ pub fn move_to_pane_on_top(&mut self) { - match self.current_pane { - Pane::Input => { - self.current_pane = Pane::Results; - } - _ => {} + if self.current_pane == Pane::Input { + self.current_pane = Pane::Results; } } @@ -233,11 +222,8 @@ impl Television { /// │ Search ││ │ /// └───────────────────┘└─────────────┘ pub fn move_to_pane_below(&mut self) { - match self.current_pane { - Pane::Results => { - self.current_pane = Pane::Input; - } - _ => {} + if self.current_pane == Pane::Results { + self.current_pane = Pane::Input; } } @@ -269,11 +255,8 @@ impl Television { /// │ Search ││ │ /// └───────────────────┘└─────────────┘ pub fn move_to_pane_left(&mut self) { - match self.current_pane { - Pane::Preview => { - self.current_pane = Pane::Results; - } - _ => {} + if self.current_pane == Pane::Preview { + self.current_pane = Pane::Results; } } @@ -350,7 +333,7 @@ impl Component for Television { Action::AddInputChar(_) | Action::DeletePrevChar | Action::DeleteNextChar => { let new_pattern = self.input.value().to_string(); if new_pattern != self.current_pattern { - self.current_pattern = new_pattern.clone(); + self.current_pattern.clone_from(&new_pattern); self.find(&new_pattern); self.reset_preview_scroll(); self.picker_state.select(Some(0)); @@ -379,9 +362,9 @@ impl Component for Television { } fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let (results_area, input_area, preview_title_area, preview_area) = self.create_layout(area); + let (results_area, input_area, preview_title_area, preview_area) = create_layout(area); - self.results_area_height = results_area.height as u32; + self.results_area_height = u32::from(results_area.height); self.preview_pane_height = preview_area.height; // top left block: results @@ -393,7 +376,7 @@ impl Component for Television { ) .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(self.get_border_style(Pane::Results == self.current_pane)) + .border_style(get_border_style(Pane::Results == self.current_pane)) .style(Style::default()) .padding(Padding::right(1)); @@ -404,9 +387,9 @@ impl Component for Television { let entries = self.channel.results( (results_area.height - 2).into(), - self.picker_view_offset as u32, + u32::try_from(self.picker_view_offset).unwrap(), ); - let results_list = self.build_results_list(results_block, &entries); + let results_list = build_results_list(results_block, &entries); frame.render_stateful_widget(results_list, results_area, &mut self.relative_picker_state); @@ -419,7 +402,7 @@ impl Component for Television { ) .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(self.get_border_style(Pane::Input == self.current_pane)) + .border_style(get_border_style(Pane::Input == self.current_pane)) .style(Style::default()); let input_block_inner = input_block.inner(input_area); @@ -445,7 +428,7 @@ impl Component for Television { let width = inner_input_chunks[1].width.max(3) - 3; // keep 2 for borders and 1 for cursor let scroll = self.input.visual_scroll(width as usize); let input = Paragraph::new(self.input.value()) - .scroll((0, scroll as u16)) + .scroll((0, u16::try_from(scroll).unwrap())) .block(interactive_input_block) .style(Style::default().fg(DEFAULT_INPUT_FG)) .alignment(Alignment::Left); @@ -473,10 +456,10 @@ impl Component for Television { frame.set_cursor_position(( // Put cursor past the end of the input text inner_input_chunks[1].x - + ((self.input.visual_cursor()).max(scroll) - scroll) as u16, + + u16::try_from((self.input.visual_cursor()).max(scroll) - scroll).unwrap(), // Move one line down, from the border to the input line inner_input_chunks[1].y, - )) + )); } // top right block: preview title @@ -489,7 +472,7 @@ impl Component for Television { Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(self.get_border_style(false)), + .border_style(get_border_style(false)), ) .style(Style::default().fg(DEFAULT_PREVIEW_TITLE_FG)) .alignment(Alignment::Left); @@ -504,7 +487,7 @@ impl Component for Television { ) .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(self.get_border_style(Pane::Preview == self.current_pane)) + .border_style(get_border_style(Pane::Preview == self.current_pane)) .style(Style::default()) .padding(Padding::right(1)); @@ -517,169 +500,34 @@ impl Component for Television { let inner = preview_outer_block.inner(preview_area); frame.render_widget(preview_outer_block, preview_area); - match &preview.content { - PreviewContent::Image(img) => { - let image_component = StatefulImage::new(None); - frame.render_stateful_widget(image_component, inner, &mut img.clone()); - } - _ => { - let preview_block = self.build_preview_paragraph( - preview_inner_block, - &inner, - preview, - selected_entry.line_number.map(|l| l as u16), - ); - frame.render_widget(preview_block, inner); - } + if let PreviewContent::Image(img) = &preview.content { + let image_component = StatefulImage::new(None); + frame.render_stateful_widget(image_component, inner, &mut img.clone()); + } else { + let preview_block = self.build_preview_paragraph( + preview_inner_block, + inner, + &preview, + selected_entry + .line_number + // FIXME: this actually might panic in some edge cases + .map(|l| u16::try_from(l).unwrap()), + ); + frame.render_widget(preview_block, inner); } - Ok(()) } } impl Television { - fn get_border_style(&self, focused: bool) -> Style { - if focused { - Style::default().fg(Color::Green) - } else { - // TODO: make this depend on self.config - Style::default().fg(Color::Rgb(90, 90, 110)).dim() - } - } - - fn create_layout(&self, area: Rect) -> (Rect, Rect, Rect, Rect) { - let main_block = centered_rect(UI_WIDTH_PERCENT, UI_HEIGHT_PERCENT, area); - - // split the main block into two vertical chunks - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(main_block); - - // left block: results + input field - let left_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(10), Constraint::Length(3)]) - .split(chunks[0]); - - // right block: preview title + preview - let right_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(3), Constraint::Min(10)]) - .split(chunks[1]); - - ( - left_chunks[0], - left_chunks[1], - right_chunks[0], - right_chunks[1], - ) - } - - fn build_results_list<'a, 'b>( - &self, - results_block: Block<'b>, - entries: &'a Vec, - ) -> List<'a> - where - 'b: 'a, - { - List::new(entries.iter().map(|entry| { - let mut spans = Vec::new(); - // optional icon - if let Some(icon) = &entry.icon { - spans.push(Span::styled( - icon.to_string(), - Style::default().fg(Color::from_str(icon.color).unwrap()), - )); - spans.push(Span::raw(" ")); - } - // entry name - if let Some(name_match_ranges) = &entry.name_match_ranges { - let mut last_match_end = 0; - for (start, end) in name_match_ranges - .iter() - .map(|(s, e)| (*s as usize, *e as usize)) - { - spans.push(Span::styled( - slice_at_char_boundaries(&entry.name, last_match_end, start), - Style::default().fg(DEFAULT_RESULT_NAME_FG), - )); - spans.push(Span::styled( - slice_at_char_boundaries(&entry.name, start, end), - Style::default().fg(Color::Red), - )); - last_match_end = end; - } - spans.push(Span::styled( - &entry.name[next_char_boundary(&entry.name, last_match_end)..], - Style::default().fg(DEFAULT_RESULT_NAME_FG), - )); - } else { - spans.push(Span::styled( - entry.display_name(), - Style::default().fg(DEFAULT_RESULT_NAME_FG), - )); - } - // optional line number - if let Some(line_number) = entry.line_number { - spans.push(Span::styled( - format!(":{}", line_number), - Style::default().fg(DEFAULT_RESULT_LINE_NUMBER_FG), - )); - } - // optional preview - if let Some(preview) = &entry.preview { - spans.push(Span::raw(": ")); - - if let Some(preview_match_ranges) = &entry.preview_match_ranges { - if !preview_match_ranges.is_empty() { - let mut last_match_end = 0; - for (start, end) in preview_match_ranges - .iter() - .map(|(s, e)| (*s as usize, *e as usize)) - { - spans.push(Span::styled( - slice_at_char_boundaries(preview, last_match_end, start), - Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), - )); - spans.push(Span::styled( - slice_at_char_boundaries(preview, start, end), - Style::default().fg(Color::Red), - )); - last_match_end = end; - } - spans.push(Span::styled( - &preview[next_char_boundary( - &preview, - preview_match_ranges.last().unwrap().1 as usize, - )..], - Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), - )); - } - } else { - spans.push(Span::styled( - preview, - Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), - )); - } - } - Line::from(spans) - })) - .direction(ListDirection::BottomToTop) - .highlight_style(Style::default().bg(Color::Rgb(50, 50, 50))) - .highlight_symbol("> ") - .block(results_block) - } - const FILL_CHAR_SLANTED: char = '╱'; const FILL_CHAR_EMPTY: char = ' '; fn build_preview_paragraph<'b>( &'b mut self, preview_block: Block<'b>, - inner: &Rect, - preview: Arc, + inner: Rect, + preview: &Arc, target_line: Option, ) -> Paragraph<'b> { self.maybe_init_preview_scroll(target_line, inner.height); @@ -689,7 +537,9 @@ impl Television { for (i, line) in content.iter().enumerate() { lines.push(Line::from(vec![ build_line_number_span(i + 1).style(Style::default().fg( - if matches!(target_line, Some(l) if l == i as u16 + 1) { + // FIXME: this actually might panic in some edge cases + if matches!(target_line, Some(l) if l == u16::try_from(i).unwrap() + 1) + { DEFAULT_PREVIEW_GUTTER_SELECTED_FG } else { DEFAULT_PREVIEW_GUTTER_FG @@ -699,7 +549,7 @@ impl Television { Span::styled( line.to_string(), Style::default().fg(DEFAULT_PREVIEW_CONTENT_FG).bg( - if matches!(target_line, Some(l) if l == i as u16 + 1) { + if matches!(target_line, Some(l) if l == u16::try_from(i).unwrap() + 1) { DEFAULT_SELECTED_PREVIEW_BG } else { Color::Reset @@ -755,7 +605,6 @@ impl Television { .block(preview_block) .alignment(Alignment::Left) .style(Style::default().add_modifier(Modifier::ITALIC)), - PreviewContent::Empty => Paragraph::new(Text::raw(EMPTY_STRING)), _ => Paragraph::new(Text::raw(EMPTY_STRING)), } } @@ -768,7 +617,7 @@ impl Television { fn build_meta_preview_paragraph<'a>( &mut self, - inner: &Rect, + inner: Rect, message: &str, fill_char: char, ) -> Paragraph<'a> { @@ -843,11 +692,11 @@ impl InputActionHandler for Input { } fn build_line_number_span<'a>(line_number: usize) -> Span<'a> { - Span::from(format!("{:5} ", line_number)) + Span::from(format!("{line_number:5} ")) } fn compute_paragraph_from_highlighted_lines( - highlighted_lines: &Vec>, + highlighted_lines: &[Vec<(syntect::highlighting::Style, String)>], line_specifier: Option, ) -> Paragraph<'static> { let preview_lines: Vec = highlighted_lines @@ -869,7 +718,7 @@ fn compute_paragraph_from_highlighted_lines( ))) .chain(l.iter().cloned().map(|sr| { convert_syn_region_to_span( - (sr.0, sr.1.replace("\t", FOUR_SPACES)), + &(sr.0, sr.1.replace('\t', FOUR_SPACES)), if line_specifier.is_some() && i == line_specifier.unwrap() - 1 { Some(SyntectColor { r: 50, @@ -890,7 +739,7 @@ fn compute_paragraph_from_highlighted_lines( } fn convert_syn_region_to_span<'a>( - syn_region: (syntect::highlighting::Style, String), + syn_region: &(syntect::highlighting::Style, String), background: Option, ) -> Span<'a> { let mut style = @@ -912,23 +761,3 @@ fn convert_syn_color_to_ratatui_color( ) -> ratatui::style::Color { ratatui::style::Color::Rgb(color.r, color.g, color.b) } - -fn next_char_boundary(s: &str, start: usize) -> usize { - let mut i = start; - while !s.is_char_boundary(i) { - i += 1; - } - i -} - -fn prev_char_boundary(s: &str, start: usize) -> usize { - let mut i = start; - while !s.is_char_boundary(i) { - i -= 1; - } - i -} - -fn slice_at_char_boundaries(s: &str, start_byte_index: usize, end_byte_index: usize) -> &str { - &s[prev_char_boundary(s, start_byte_index)..next_char_boundary(s, end_byte_index)] -} diff --git a/src/components/ui.rs b/src/components/ui.rs new file mode 100644 index 0000000..3deca17 --- /dev/null +++ b/src/components/ui.rs @@ -0,0 +1,160 @@ +use ratatui::{ + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Style, Stylize}, + text::{Line, Span}, + widgets::{Block, List, ListDirection}, +}; +use std::str::FromStr; + +use crate::components::finders::Entry; +use crate::components::utils::strings::{next_char_boundary, slice_at_char_boundaries}; +use crate::components::utils::ui::centered_rect; + +// UI size +const UI_WIDTH_PERCENT: u16 = 95; +const UI_HEIGHT_PERCENT: u16 = 95; + +// Styles +// results +const DEFAULT_RESULT_NAME_FG: Color = Color::Blue; +const DEFAULT_RESULT_PREVIEW_FG: Color = Color::Rgb(150, 150, 150); +const DEFAULT_RESULT_LINE_NUMBER_FG: Color = Color::Yellow; +// input +const DEFAULT_INPUT_FG: Color = Color::Rgb(200, 200, 200); +const DEFAULT_RESULTS_COUNT_FG: Color = Color::Rgb(150, 150, 150); +// preview +const DEFAULT_PREVIEW_TITLE_FG: Color = Color::Blue; +const DEFAULT_SELECTED_PREVIEW_BG: Color = Color::Rgb(50, 50, 50); +const DEFAULT_PREVIEW_CONTENT_FG: Color = Color::Rgb(150, 150, 180); +const DEFAULT_PREVIEW_GUTTER_FG: Color = Color::Rgb(70, 70, 70); +const DEFAULT_PREVIEW_GUTTER_SELECTED_FG: Color = Color::Rgb(255, 150, 150); + +pub fn get_border_style(focused: bool) -> Style { + if focused { + Style::default().fg(Color::Green) + } else { + // TODO: make this depend on self.config + Style::default().fg(Color::Rgb(90, 90, 110)).dim() + } +} + +pub fn create_layout(area: Rect) -> (Rect, Rect, Rect, Rect) { + let main_block = centered_rect(UI_WIDTH_PERCENT, UI_HEIGHT_PERCENT, area); + + // split the main block into two vertical chunks + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(main_block); + + // left block: results + input field + let left_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(10), Constraint::Length(3)]) + .split(chunks[0]); + + // right block: preview title + preview + let right_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(3), Constraint::Min(10)]) + .split(chunks[1]); + + ( + left_chunks[0], + left_chunks[1], + right_chunks[0], + right_chunks[1], + ) +} + +pub fn build_results_list<'a, 'b>(results_block: Block<'b>, entries: &'a [Entry]) -> List<'a> +where + 'b: 'a, +{ + List::new(entries.iter().map(|entry| { + let mut spans = Vec::new(); + // optional icon + if let Some(icon) = &entry.icon { + spans.push(Span::styled( + icon.to_string(), + Style::default().fg(Color::from_str(icon.color).unwrap()), + )); + spans.push(Span::raw(" ")); + } + // entry name + if let Some(name_match_ranges) = &entry.name_match_ranges { + let mut last_match_end = 0; + for (start, end) in name_match_ranges + .iter() + .map(|(s, e)| (*s as usize, *e as usize)) + { + spans.push(Span::styled( + slice_at_char_boundaries(&entry.name, last_match_end, start), + Style::default().fg(DEFAULT_RESULT_NAME_FG).bold().italic(), + )); + spans.push(Span::styled( + slice_at_char_boundaries(&entry.name, start, end), + Style::default().fg(Color::Red).bold().italic(), + )); + last_match_end = end; + } + spans.push(Span::styled( + &entry.name[next_char_boundary(&entry.name, last_match_end)..], + Style::default().fg(DEFAULT_RESULT_NAME_FG).bold().italic(), + )); + } else { + spans.push(Span::styled( + entry.display_name(), + Style::default().fg(DEFAULT_RESULT_NAME_FG).bold().italic(), + )); + } + // optional line number + if let Some(line_number) = entry.line_number { + spans.push(Span::styled( + format!(":{line_number}"), + Style::default().fg(DEFAULT_RESULT_LINE_NUMBER_FG), + )); + } + // optional preview + if let Some(preview) = &entry.preview { + spans.push(Span::raw(": ")); + + if let Some(preview_match_ranges) = &entry.preview_match_ranges { + if !preview_match_ranges.is_empty() { + let mut last_match_end = 0; + for (start, end) in preview_match_ranges + .iter() + .map(|(s, e)| (*s as usize, *e as usize)) + { + spans.push(Span::styled( + slice_at_char_boundaries(preview, last_match_end, start), + Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), + )); + spans.push(Span::styled( + slice_at_char_boundaries(preview, start, end), + Style::default().fg(Color::Red), + )); + last_match_end = end; + } + spans.push(Span::styled( + &preview[next_char_boundary( + preview, + preview_match_ranges.last().unwrap().1 as usize, + )..], + Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), + )); + } + } else { + spans.push(Span::styled( + preview, + Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), + )); + } + } + Line::from(spans) + })) + .direction(ListDirection::BottomToTop) + .highlight_style(Style::default().bg(Color::Rgb(50, 50, 50))) + .highlight_symbol("> ") + .block(results_block) +} diff --git a/src/components/utils/files.rs b/src/components/utils/files.rs index 15c932f..815f904 100644 --- a/src/components/utils/files.rs +++ b/src/components/utils/files.rs @@ -40,17 +40,13 @@ pub fn is_not_text(bytes: &[u8]) -> Option { match infer.get(bytes) { Some(t) => { let mime_type = t.mime_type(); - if mime_type.contains("image") { - Some(true) - } else if mime_type.contains("video") { - Some(true) - } else if mime_type.contains("audio") { - Some(true) - } else if mime_type.contains("archive") { - Some(true) - } else if mime_type.contains("book") { - Some(true) - } else if mime_type.contains("font") { + if mime_type.contains("image") + || mime_type.contains("video") + || mime_type.contains("audio") + || mime_type.contains("archive") + || mime_type.contains("book") + || mime_type.contains("font") + { Some(true) } else { None @@ -64,6 +60,12 @@ pub fn is_valid_utf8(bytes: &[u8]) -> bool { std::str::from_utf8(bytes).is_ok() } +pub fn is_known_text_extension(path: &Path) -> bool { + path.extension() + .and_then(|ext| ext.to_str()) + .is_some_and(|ext| KNOWN_TEXT_FILE_EXTENSIONS.contains(ext)) +} + lazy_static! { static ref KNOWN_TEXT_FILE_EXTENSIONS: HashSet<&'static str> = [ "ada", @@ -395,10 +397,3 @@ lazy_static! { ] .into(); } - -pub fn is_known_text_extension(path: &Path) -> bool { - path.extension() - .and_then(|ext| ext.to_str()) - .map(|ext| KNOWN_TEXT_FILE_EXTENSIONS.contains(&ext)) - .unwrap_or(false) -} diff --git a/src/components/utils/strings.rs b/src/components/utils/strings.rs index 849ef11..43d3c09 100644 --- a/src/components/utils/strings.rs +++ b/src/components/utils/strings.rs @@ -1,7 +1,27 @@ use lazy_static::lazy_static; use std::fmt::Write; -pub fn slice_at_char_boundary(s: &str, byte_index: usize) -> &str { +pub fn next_char_boundary(s: &str, start: usize) -> usize { + let mut i = start; + while !s.is_char_boundary(i) { + i += 1; + } + i +} + +pub fn prev_char_boundary(s: &str, start: usize) -> usize { + let mut i = start; + while !s.is_char_boundary(i) { + i -= 1; + } + i +} + +pub fn slice_at_char_boundaries(s: &str, start_byte_index: usize, end_byte_index: usize) -> &str { + &s[prev_char_boundary(s, start_byte_index)..next_char_boundary(s, end_byte_index)] +} + +pub fn slice_up_to_char_boundary(s: &str, byte_index: usize) -> &str { let mut char_index = byte_index; while !s.is_char_boundary(char_index) { char_index -= 1; @@ -84,12 +104,12 @@ pub fn preprocess_line(line: &str) -> String { replace_nonprintable( { if line.len() > MAX_LINE_LENGTH { - slice_at_char_boundary(line, MAX_LINE_LENGTH) + slice_up_to_char_boundary(line, MAX_LINE_LENGTH) } else { line } } - .trim_end_matches(|c| c == '\r' || c == '\n' || c == '\0') + .trim_end_matches(['\r', '\n', '\0']) .as_bytes(), 2, )