mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
feat: incsearch & hlsearch
This commit is contained in:
parent
782f88b965
commit
749b1e8cce
5 changed files with 99 additions and 34 deletions
|
|
@ -2,27 +2,32 @@ use core::files::File;
|
||||||
|
|
||||||
use config::{MANAGER, THEME};
|
use config::{MANAGER, THEME};
|
||||||
use ratatui::{buffer::Buffer, layout::Rect, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{List, ListItem, Widget}};
|
use ratatui::{buffer::Buffer, layout::Rect, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{List, ListItem, Widget}};
|
||||||
use shared::short_path;
|
use shared::{short_path, short_path_parts, PathParts};
|
||||||
|
|
||||||
use crate::Ctx;
|
use crate::Ctx;
|
||||||
|
|
||||||
pub(super) struct Folder<'a> {
|
pub(super) struct Folder<'a> {
|
||||||
cx: &'a Ctx,
|
cx: &'a Ctx,
|
||||||
folder: &'a core::manager::Folder,
|
folder: &'a core::manager::Folder,
|
||||||
is_preview: bool,
|
location: FolderLocation,
|
||||||
is_selection: bool,
|
is_selection: bool,
|
||||||
is_find: bool,
|
is_find: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Folder<'a> {
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||||
pub(super) fn new(cx: &'a Ctx, folder: &'a core::manager::Folder) -> Self {
|
pub(super) enum FolderLocation {
|
||||||
Self { cx, folder, is_preview: false, is_selection: false, is_find: false }
|
Parent,
|
||||||
}
|
Current,
|
||||||
|
Preview,
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
impl<'a> Folder<'a> {
|
||||||
pub(super) fn with_preview(mut self, state: bool) -> Self {
|
pub(super) fn new(
|
||||||
self.is_preview = state;
|
cx: &'a Ctx,
|
||||||
self
|
folder: &'a core::manager::Folder,
|
||||||
|
location: FolderLocation,
|
||||||
|
) -> Self {
|
||||||
|
Self { cx, folder, location, is_selection: false, is_find: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
@ -66,7 +71,10 @@ impl<'a> Widget for Folder<'a> {
|
||||||
let active = self.cx.manager.active();
|
let active = self.cx.manager.active();
|
||||||
let mode = active.mode();
|
let mode = active.mode();
|
||||||
|
|
||||||
let window = if self.is_preview {
|
// TODO to be configured by THEME?
|
||||||
|
let find_style = Style::new().fg(Color::Rgb(255, 255, 50)).add_modifier(Modifier::ITALIC);
|
||||||
|
|
||||||
|
let window = if self.location == FolderLocation::Preview {
|
||||||
self.folder.window_for(active.preview().skip())
|
self.folder.window_for(active.preview().skip())
|
||||||
} else {
|
} else {
|
||||||
self.folder.window()
|
self.folder.window()
|
||||||
|
|
@ -91,7 +99,7 @@ impl<'a> Widget for Folder<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let hovered = matches!(self.folder.hovered, Some(ref h) if h.url() == f.url());
|
let hovered = matches!(self.folder.hovered, Some(ref h) if h.url() == f.url());
|
||||||
let style = if self.is_preview && hovered {
|
let style = if self.location == FolderLocation::Preview && hovered {
|
||||||
THEME.preview.hovered.get()
|
THEME.preview.hovered.get()
|
||||||
} else if hovered {
|
} else if hovered {
|
||||||
THEME.selection.hovered.get()
|
THEME.selection.hovered.get()
|
||||||
|
|
@ -102,7 +110,24 @@ impl<'a> Widget for Folder<'a> {
|
||||||
let mut spans = Vec::with_capacity(10);
|
let mut spans = Vec::with_capacity(10);
|
||||||
|
|
||||||
spans.push(Span::raw(format!(" {} ", Self::icon(f))));
|
spans.push(Span::raw(format!(" {} ", Self::icon(f))));
|
||||||
|
if let Some((path, (n_prefix, n_hl, n_suffix))) =
|
||||||
|
// If this is the current folder,
|
||||||
|
(self.location == FolderLocation::Current).then_some(()).and_then(|_| {
|
||||||
|
// ... and we are now finding something,
|
||||||
|
let finder = active.finder()?;
|
||||||
|
// ... and we can get the short path parts,
|
||||||
|
let PathParts { path, filename } = short_path_parts(f.url(), &self.folder.cwd)?;
|
||||||
|
// ... and we can get the highlight range,
|
||||||
|
Some((path, finder.try_render_highlight(filename)?))
|
||||||
|
}) {
|
||||||
|
// ... then render the highlighted short path.
|
||||||
|
spans.push(Span::raw(path.join(n_prefix).display().to_string()));
|
||||||
|
spans.push(Span::styled(n_hl, find_style));
|
||||||
|
spans.push(Span::raw(n_suffix));
|
||||||
|
} else {
|
||||||
|
// Otherwise, just render the short path.
|
||||||
spans.push(Span::raw(short_path(f.url(), &self.folder.cwd)));
|
spans.push(Span::raw(short_path(f.url(), &self.folder.cwd)));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(link_to) = f.link_to() {
|
if let Some(link_to) = f.link_to() {
|
||||||
if MANAGER.show_symlink {
|
if MANAGER.show_symlink {
|
||||||
|
|
@ -110,22 +135,23 @@ impl<'a> Widget for Folder<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hovered && self.is_find {
|
||||||
if let Some(idx) = active
|
if let Some(idx) = active
|
||||||
.finder()
|
.finder()
|
||||||
.filter(|&f| hovered && self.is_find && f.has_matched())
|
.filter(|&f| f.has_matched())
|
||||||
.and_then(|finder| finder.matched_idx(f.url()))
|
.and_then(|finder| finder.matched_idx(f.url()))
|
||||||
{
|
{
|
||||||
let len = active.finder().unwrap().matched().len();
|
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(
|
spans.push(Span::styled(
|
||||||
format!(
|
format!(
|
||||||
" [{}/{}]",
|
" [{}/{}]",
|
||||||
if idx > 99 { ">99".to_string() } else { (idx + 1).to_string() },
|
if idx > 99 { ">99".to_string() } else { (idx + 1).to_string() },
|
||||||
if len > 99 { ">99".to_string() } else { len.to_string() }
|
if len > 99 { ">99".to_string() } else { len.to_string() }
|
||||||
),
|
),
|
||||||
style,
|
find_style,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ListItem::new(Line::from(spans)).style(style)
|
ListItem::new(Line::from(spans)).style(style)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use config::MANAGER;
|
||||||
use ratatui::{buffer::Buffer, layout::{self, Constraint, Direction, Rect}, widgets::{Block, Borders, Padding, Widget}};
|
use ratatui::{buffer::Buffer, layout::{self, Constraint, Direction, Rect}, widgets::{Block, Borders, Padding, Widget}};
|
||||||
|
|
||||||
use super::{Folder, Preview};
|
use super::{Folder, Preview};
|
||||||
use crate::Ctx;
|
use crate::{manager::folder::FolderLocation, Ctx};
|
||||||
|
|
||||||
pub(crate) struct Layout<'a> {
|
pub(crate) struct Layout<'a> {
|
||||||
cx: &'a Ctx,
|
cx: &'a Ctx,
|
||||||
|
|
@ -32,12 +32,12 @@ impl<'a> Widget for Layout<'a> {
|
||||||
// Parent
|
// Parent
|
||||||
let block = Block::new().borders(Borders::RIGHT).padding(Padding::new(1, 0, 0, 0));
|
let block = Block::new().borders(Borders::RIGHT).padding(Padding::new(1, 0, 0, 0));
|
||||||
if let Some(parent) = manager.parent() {
|
if let Some(parent) = manager.parent() {
|
||||||
Folder::new(self.cx, parent).render(block.inner(chunks[0]), buf);
|
Folder::new(self.cx, parent, FolderLocation::Parent).render(block.inner(chunks[0]), buf);
|
||||||
}
|
}
|
||||||
block.render(chunks[0], buf);
|
block.render(chunks[0], buf);
|
||||||
|
|
||||||
// Current
|
// Current
|
||||||
Folder::new(self.cx, manager.current())
|
Folder::new(self.cx, manager.current(), FolderLocation::Current)
|
||||||
.with_selection(manager.active().mode().is_visual())
|
.with_selection(manager.active().mode().is_visual())
|
||||||
.with_find(manager.active().finder().is_some())
|
.with_find(manager.active().finder().is_some())
|
||||||
.render(chunks[1], buf);
|
.render(chunks[1], buf);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use ansi_to_tui::IntoText;
|
||||||
use ratatui::{buffer::Buffer, layout::Rect, widgets::{Paragraph, Widget}};
|
use ratatui::{buffer::Buffer, layout::Rect, widgets::{Paragraph, Widget}};
|
||||||
|
|
||||||
use super::Folder;
|
use super::Folder;
|
||||||
use crate::Ctx;
|
use crate::{manager::folder::FolderLocation, Ctx};
|
||||||
|
|
||||||
pub(super) struct Preview<'a> {
|
pub(super) struct Preview<'a> {
|
||||||
cx: &'a Ctx,
|
cx: &'a Ctx,
|
||||||
|
|
@ -29,7 +29,7 @@ impl<'a> Widget for Preview<'a> {
|
||||||
match &preview.lock.as_ref().unwrap().data {
|
match &preview.lock.as_ref().unwrap().data {
|
||||||
PreviewData::Folder => {
|
PreviewData::Folder => {
|
||||||
if let Some(folder) = manager.active().history(hovered) {
|
if let Some(folder) = manager.active().history(hovered) {
|
||||||
Folder::new(self.cx, folder).with_preview(true).render(area, buf);
|
Folder::new(self.cx, folder, FolderLocation::Preview).render(area, buf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PreviewData::Text(s) => {
|
PreviewData::Text(s) => {
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,29 @@ impl Finder {
|
||||||
self.query.is_match(name.as_bytes())
|
self.query.is_match(name.as_bytes())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Try to render the highlight range for the given name.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// (prefix, highlight, suffix)
|
||||||
|
pub fn try_render_highlight(&self, name: &OsStr) -> Option<(String, String, String)> {
|
||||||
|
let name_bytes;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
name_bytes = name.to_string_lossy().as_bytes();
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
name_bytes = name.as_bytes();
|
||||||
|
}
|
||||||
|
let range = self.query.find(name_bytes).map(|m| m.range())?;
|
||||||
|
Some((
|
||||||
|
String::from_utf8_lossy(&name_bytes[..range.start]).into_owned(),
|
||||||
|
String::from_utf8_lossy(&name_bytes[range.start..range.end]).into_owned(),
|
||||||
|
String::from_utf8_lossy(&name_bytes[range.end..]).into_owned(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Finder {
|
impl Finder {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{borrow::Cow, env, path::{Component, Path, PathBuf}};
|
use std::{env, ffi::OsStr, path::{Component, Path, PathBuf}};
|
||||||
|
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
|
|
@ -23,6 +23,22 @@ pub fn expand_url(mut u: Url) -> Url {
|
||||||
u
|
u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct PathParts<'a> {
|
||||||
|
pub path: &'a Path,
|
||||||
|
pub filename: &'a OsStr,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn short_path_parts<'a>(p: &'a Path, base: &Path) -> Option<PathParts<'a>> {
|
||||||
|
let p = p.strip_prefix(base).unwrap_or(p);
|
||||||
|
let mut parts = p.components();
|
||||||
|
let filename = parts.next_back().and_then(|p| match p {
|
||||||
|
Component::Normal(p) => Some(p),
|
||||||
|
_ => None,
|
||||||
|
})?;
|
||||||
|
let rest = parts.as_path();
|
||||||
|
Some(PathParts { path: rest, filename })
|
||||||
|
}
|
||||||
|
|
||||||
pub fn short_path(p: &Path, base: &Path) -> String {
|
pub fn short_path(p: &Path, base: &Path) -> String {
|
||||||
if let Ok(p) = p.strip_prefix(base) {
|
if let Ok(p) = p.strip_prefix(base) {
|
||||||
return p.display().to_string();
|
return p.display().to_string();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue