From f9eeac611fae858d07453efad61e6fdc3ddbcc10 Mon Sep 17 00:00:00 2001 From: LightQuantum Date: Tue, 26 Sep 2023 17:43:32 -0700 Subject: [PATCH] feat: highlight matching words on finding (#211) --- app/src/manager/folder.rs | 30 ++++++++++++++++++++++++------ core/src/manager/finder.rs | 22 +++++++++++++++++++++- shared/src/fns.rs | 27 ++++++++++++++++++++++----- 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/app/src/manager/folder.rs b/app/src/manager/folder.rs index 68bafe86..31b633cd 100644 --- a/app/src/manager/folder.rs +++ b/app/src/manager/folder.rs @@ -50,7 +50,7 @@ impl<'a> Folder<'a> { } #[inline] - fn file_style(&self, file: &File) -> Style { + fn item_style(&self, file: &File) -> Style { let mimetype = &self.cx.manager.mimetype; THEME .filetypes @@ -59,6 +59,25 @@ impl<'a> Folder<'a> { .map(|x| x.style.get()) .unwrap_or_else(Style::new) } + + fn highlighted_item<'b>(&'b self, file: &'b File) -> Vec { + let short = short_path(file.url(), &self.folder.cwd); + + let v = self.is_find.then_some(()).and_then(|_| { + let finder = self.cx.manager.active().finder()?; + let (head, body, tail) = finder.explode(short.name)?; + + // TODO: to be configured by THEME? + let style = Style::new().fg(Color::Rgb(255, 255, 50)).add_modifier(Modifier::ITALIC); + Some(vec![ + Span::raw(short.prefix.join(head.as_ref()).display().to_string()), + Span::styled(body, style), + Span::raw(tail), + ]) + }); + + v.unwrap_or_else(|| vec![Span::raw(format!("{}", short))]) + } } impl<'a> Widget for Folder<'a> { @@ -96,13 +115,12 @@ impl<'a> Widget for Folder<'a> { } else if hovered { THEME.selection.hovered.get() } else { - self.file_style(f) + self.item_style(f) }; let mut spans = Vec::with_capacity(10); - spans.push(Span::raw(format!(" {} ", Self::icon(f)))); - spans.push(Span::raw(short_path(f.url(), &self.folder.cwd))); + spans.extend(self.highlighted_item(f)); if let Some(link_to) = f.link_to() { if MANAGER.show_symlink { @@ -116,14 +134,14 @@ impl<'a> Widget for Folder<'a> { .and_then(|finder| finder.matched_idx(f.url())) { let len = active.finder().unwrap().matched().len(); - let style = Style::new().fg(Color::Rgb(255, 255, 50)).add_modifier(Modifier::ITALIC); spans.push(Span::styled( format!( " [{}/{}]", if idx > 99 { ">99".to_string() } else { (idx + 1).to_string() }, if len > 99 { ">99".to_string() } else { len.to_string() } ), - style, + // TODO: to be configured by THEME? + Style::new().fg(Color::Rgb(255, 255, 50)).add_modifier(Modifier::ITALIC), )); } diff --git a/core/src/manager/finder.rs b/core/src/manager/finder.rs index 01a6b85a..e6a7cea1 100644 --- a/core/src/manager/finder.rs +++ b/core/src/manager/finder.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeMap, ffi::OsStr}; +use std::{borrow::Cow, collections::BTreeMap, ffi::OsStr}; use anyhow::Result; use regex::bytes::Regex; @@ -108,6 +108,26 @@ impl Finder { self.query.is_match(name.as_bytes()) } } + + /// Explode the name into three parts: head, body, tail. + #[inline] + pub fn explode<'a>(&self, name: &'a OsStr) -> Option<(Cow<'a, str>, Cow<'a, str>, Cow<'a, str>)> { + #[cfg(target_os = "windows")] + let b = { name.to_string_lossy().as_bytes() }; + + #[cfg(not(target_os = "windows"))] + let b = { + use std::os::unix::ffi::OsStrExt; + name.as_bytes() + }; + + let range = self.query.find(b).map(|m| m.range())?; + Some(( + String::from_utf8_lossy(&b[..range.start]), + String::from_utf8_lossy(&b[range.start..range.end]), + String::from_utf8_lossy(&b[range.end..]), + )) + } } impl Finder { diff --git a/shared/src/fns.rs b/shared/src/fns.rs index be1daa28..2ac3234d 100644 --- a/shared/src/fns.rs +++ b/shared/src/fns.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, env, path::{Component, Path, PathBuf}}; +use std::{borrow::Cow, env, ffi::OsStr, fmt::Display, path::{Component, Path, PathBuf}}; use tokio::fs; @@ -23,11 +23,28 @@ pub fn expand_url(mut u: Url) -> Url { u } -pub fn short_path(p: &Path, base: &Path) -> String { - if let Ok(p) = p.strip_prefix(base) { - return p.display().to_string(); +pub struct ShortPath<'a> { + pub prefix: &'a Path, + pub name: &'a OsStr, +} + +impl Display for ShortPath<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.prefix == Path::new("") { + return write!(f, "{}", self.name.to_string_lossy()); + } + write!(f, "{}/{}", self.prefix.display(), self.name.to_string_lossy()) } - p.display().to_string() +} + +pub fn short_path<'a>(p: &'a Path, base: &Path) -> ShortPath<'a> { + let p = p.strip_prefix(base).unwrap_or(p); + let mut parts = p.components(); + let name = parts.next_back().and_then(|p| match p { + Component::Normal(p) => Some(p), + _ => None, + }); + ShortPath { prefix: parts.as_path(), name: name.unwrap_or_default() } } pub fn readable_path(p: &Path) -> String {