diff --git a/yazi-actor/src/mgr/peek.rs b/yazi-actor/src/mgr/peek.rs index 3044c2691..61702e5cf 100644 --- a/yazi-actor/src/mgr/peek.rs +++ b/yazi-actor/src/mgr/peek.rs @@ -2,7 +2,7 @@ use anyhow::Result; use yazi_macro::succ; use yazi_parser::mgr::PeekOpt; use yazi_proxy::HIDER; -use yazi_shared::data::Data; +use yazi_shared::{data::Data, url::AsUrl}; use crate::{Actor, Ctx}; @@ -25,7 +25,8 @@ impl Actor for Peek { let folder = cx.tab().hovered_folder().map(|f| (f.offset, f.cha)); if !cx.tab().preview.same_url(&hovered.url) { - cx.tab_mut().preview.skip = folder.map(|f| f.0).unwrap_or_default(); + // set matched line on the top of preview window + cx.tab_mut().preview.skip = folder.map(|f| f.0).unwrap_or_else(|| hovered.url.as_url().line().map_or(0, |l| l.saturating_sub(1))); } if !cx.tab().preview.same_file(&hovered, &mime) { cx.tab_mut().preview.reset(); diff --git a/yazi-actor/src/mgr/reveal.rs b/yazi-actor/src/mgr/reveal.rs index 7da8ea1bd..435d63213 100644 --- a/yazi-actor/src/mgr/reveal.rs +++ b/yazi-actor/src/mgr/reveal.rs @@ -2,7 +2,7 @@ use anyhow::Result; use yazi_fs::{File, FilesOp}; use yazi_macro::{act, render, succ}; use yazi_parser::mgr::RevealOpt; -use yazi_shared::{data::Data, url::UrlLike}; +use yazi_shared::{data::Data, url::{AsUrl, UrlLike}}; use crate::{Actor, Ctx}; @@ -14,6 +14,15 @@ impl Actor for Reveal { const NAME: &str = "reveal"; fn act(cx: &mut Ctx, opt: Self::Options) -> Result { + match cx.cwd().is_search() { + true => Self::reveal_search(cx, opt), + false => Self::reveal_regular(cx, opt), + } + } +} + +impl Reveal { + fn reveal_regular(cx: &mut Ctx, opt: RevealOpt) -> Result { let Some((parent, child)) = opt.target.pair() else { succ!() }; // Cd to the parent directory @@ -37,4 +46,28 @@ impl Actor for Reveal { act!(mgr:watch, cx)?; succ!(); } + + fn reveal_search(cx: &mut Ctx, opt: RevealOpt) -> Result { + let cwd = cx.cwd().clone(); + let tab = cx.tab_mut(); + let pos = tab.current.files.iter().position(|f| f.url == opt.target); + + if let Some(pos) = pos { + let delta = pos as isize - tab.current.cursor as isize; + tab.current.arrow(delta); + } else if !opt.no_dummy { + let op = FilesOp::Creating(cwd, vec![File::from_dummy(&opt.target, None)]); + tab.current.update_pub(tab.id, op); + + if let Some(pos) = tab.current.files.iter().position(|f| f.url == opt.target) { + let delta = pos as isize - tab.current.cursor as isize; + tab.current.arrow(delta); + } + } + + act!(mgr:hover, cx, None)?; + act!(mgr:peek, cx)?; + act!(mgr:watch, cx)?; + succ!(); + } } diff --git a/yazi-binding/src/macros.rs b/yazi-binding/src/macros.rs index 37e0a13f6..150baa0be 100644 --- a/yazi-binding/src/macros.rs +++ b/yazi-binding/src/macros.rs @@ -238,5 +238,10 @@ macro_rules! impl_file_methods { // TODO: use a cache Ok(yazi_config::THEME.icon.matches(me).map(Icon::from)) }); + + $methods.add_method("line", |_, me, ()| { + use yazi_shared::url::AsUrl; + Ok(me.url.as_url().line().map(|v| v as i64)) + }); }; } diff --git a/yazi-fs/src/splatter.rs b/yazi-fs/src/splatter.rs index 9db2b6cfa..2a2348404 100644 --- a/yazi-fs/src/splatter.rs +++ b/yazi-fs/src/splatter.rs @@ -2,7 +2,7 @@ use std::os::unix::ffi::{OsStrExt, OsStringExt}; #[cfg(windows)] use std::os::windows::ffi::{OsStrExt, OsStringExt}; -use std::{cell::Cell, ffi::{OsStr, OsString}, iter::{self, Peekable}, mem}; +use std::{cell::Cell, ffi::{OsStr, OsString}, fs::OpenOptions, io::Write, iter::{self, Peekable}, mem}; use yazi_shared::url::{AsUrl, Url, UrlCow}; @@ -81,6 +81,8 @@ where Some('d') | Some('D') => self.visit_dirname(it, buf), Some('t') | Some('T') => self.visit_tab(it, buf), Some('y') | Some('Y') => self.visit_yanked(it, buf), + Some('l') | Some('L') => self.visit_line_number(it, buf), + Some('c') | Some('C') => self.visit_column_number(it, buf), Some('%') => self.visit_escape(it, buf), Some('*') => self.visit_selected(it, buf), // TODO: remove this Some(c) if c.is_ascii_digit() => self.visit_digit(it, buf), @@ -192,6 +194,36 @@ where } } + fn visit_line_number(&mut self, it: &mut Iter, buf: &mut Buf) { + it.next(); + let idx = self.consume_digit(it); + + let line_str = self + .src + .selected(self.tab, idx) + .next() + .and_then(|url| url.line()) + .map(|line| line.to_string()) + .unwrap_or_else(|| "".to_string()); + + cue(buf, OsStr::new(&line_str)); + } + + fn visit_column_number(&mut self, it: &mut Iter, buf: &mut Buf) { + it.next(); + let idx = self.consume_digit(it); + + let col_str = self + .src + .selected(self.tab, idx) + .next() + .and_then(|url| url.column()) + .map(|col| col.to_string()) + .unwrap_or_else(|| "".to_string()); + + cue(buf, OsStr::new(&col_str)); + } + fn visit_escape(&mut self, it: &mut Iter, buf: &mut Buf) { buf.push(it.next().unwrap()); } fn visit_unknown(&mut self, it: &mut Iter, buf: &mut Buf) { diff --git a/yazi-plugin/preset/components/entity.lua b/yazi-plugin/preset/components/entity.lua index aad13b122..4f56a1712 100644 --- a/yazi-plugin/preset/components/entity.lua +++ b/yazi-plugin/preset/components/entity.lua @@ -43,6 +43,13 @@ end function Entity:highlights() local name, p = self._file.name, ui.printable + + local ok, line = pcall(function() return self._file:line() end) + if ok and line then + local path_str = tostring(self._file.path) + name = name .. ":" .. line .. " (" .. path_str .. ")" + end + local highlights = self._file:highlights() if not highlights or #highlights == 0 then return p(name) diff --git a/yazi-plugin/src/external/rg.rs b/yazi-plugin/src/external/rg.rs index 65c724315..277f957fe 100644 --- a/yazi-plugin/src/external/rg.rs +++ b/yazi-plugin/src/external/rg.rs @@ -2,8 +2,8 @@ use std::process::Stdio; use anyhow::Result; use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command, sync::mpsc::{self, UnboundedReceiver}}; -use yazi_fs::{File, FsUrl}; -use yazi_shared::url::{AsUrl, UrlBuf, UrlLike}; +use yazi_fs::{File}; +use yazi_shared::url::{UrlBuf, UrlLike}; use yazi_vfs::VfsFile; pub struct RgOpt { @@ -13,13 +13,24 @@ pub struct RgOpt { pub args: Vec, } +fn parse_rg_line_column(line: &str) -> Option<(&str, usize, usize)> { + let mut parts = line.split(':'); + + let (f, l, c) = (parts.next()?, parts.next()?, parts.next()?); + if f.is_empty() { return None; } + + Some((f, l.parse().ok()?, c.parse().ok()?)) +} + pub fn rg(opt: RgOpt) -> Result> { + let subject = opt.subject.clone(); + let cwd = opt.cwd.clone(); + let mut child = Command::new("rg") - .args(["--color=never", "--files-with-matches", "--smart-case"]) + .args(["--color=never", "--line-number", "--no-heading", "--smart-case", "--column"]) .arg(if opt.hidden { "--hidden" } else { "--no-hidden" }) .args(opt.args) .arg(opt.subject) - .arg(&*opt.cwd.as_url().unified_path()) .kill_on_drop(true) .stdout(Stdio::piped()) .stderr(Stdio::null()) @@ -30,9 +41,14 @@ pub fn rg(opt: RgOpt) -> Result> { tokio::spawn(async move { while let Ok(Some(line)) = it.next_line().await { - let Ok(url) = opt.cwd.try_join(line) else { - continue; - }; + let Some((fp, l, c)) = parse_rg_line_column(&line) else { continue }; + + let url = cwd.try_join(fp) + .ok() + .and_then(|u| u.to_search(&format!("{}#{}:{}", subject, l, c)).ok()); + + let Some(url) = url else { continue }; + if let Ok(file) = File::new(url).await { tx.send(file).ok(); } diff --git a/yazi-plugin/src/utils/preview.rs b/yazi-plugin/src/utils/preview.rs index eac529c9a..eb719aced 100644 --- a/yazi-plugin/src/utils/preview.rs +++ b/yazi-plugin/src/utils/preview.rs @@ -18,10 +18,7 @@ impl Utils { let path = lock.url.as_url().unified_path(); let inner = match Highlighter::new(path).highlight(lock.skip, area.size()).await { Ok(text) => text, - Err(e @ PeekError::Exceed(max)) => return (e.to_string(), max).into_lua_multi(&lua), - Err(e @ PeekError::Unexpected(_)) => { - return e.to_string().into_lua_multi(&lua); - } + Err(e) => return e.to_string().into_lua_multi(&lua), }; lock.data = vec![Renderable::Text(Text { diff --git a/yazi-shared/src/scheme/encode.rs b/yazi-shared/src/scheme/encode.rs index c51e28ead..d67992724 100644 --- a/yazi-shared/src/scheme/encode.rs +++ b/yazi-shared/src/scheme/encode.rs @@ -14,7 +14,7 @@ impl<'a> From> for Encode<'a> { impl<'a> Encode<'a> { #[inline] pub fn domain<'s>(s: &'s str) -> PercentEncode<'s> { - const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':'); + const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':').add(b'#'); percent_encode(s.as_bytes(), SET) } diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index 54fdd1e14..c7cc1e598 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -86,6 +86,36 @@ impl<'a> Url<'a> { }) } + #[inline] + pub fn line(self) -> Option { + let Self::Search { domain, .. } = self else { return None }; + let Some(hash_pos) = domain.find('#') else { + return None + }; + + let frag = &domain[hash_pos + 1..]; + if let Some(colon_pos) = frag.find(':') { + frag[..colon_pos].parse::().ok() + } else { + frag.parse::().ok() + } + } + + #[inline] + pub fn column(self) -> Option { + let Self::Search { domain, .. } = self else { return None }; + let Some(hash_pos) = domain.find('#') else { + return None + }; + + let frag = &domain[hash_pos + 1..]; + if let Some(colon_pos) = frag.find(':') { + frag[colon_pos + 1..].parse::().ok() + } else { + None + } + } + #[inline] pub fn has_base(self) -> bool { match self {