mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
Merge 529ecbe6a6 into 6aa75594db
This commit is contained in:
commit
9bba3830f6
13 changed files with 271 additions and 53 deletions
|
|
@ -25,6 +25,8 @@ impl Preview {
|
||||||
impl UserData for Preview {
|
impl UserData for Preview {
|
||||||
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
|
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
|
||||||
fields.add_field_method_get("skip", |_, me| Ok(me.skip));
|
fields.add_field_method_get("skip", |_, me| Ok(me.skip));
|
||||||
|
fields.add_field_method_get("search_idx", |_, me| Ok(me.search_idx));
|
||||||
|
|
||||||
fields.add_static_field("folder", |_, me| {
|
fields.add_static_field("folder", |_, me| {
|
||||||
me.tab
|
me.tab
|
||||||
.hovered_folder()
|
.hovered_folder()
|
||||||
|
|
|
||||||
|
|
@ -37,13 +37,13 @@ impl Actor for Peek {
|
||||||
succ!();
|
succ!();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let preview = &mut cx.tab_mut().preview;
|
||||||
if let Some(skip) = form.skip {
|
if let Some(skip) = form.skip {
|
||||||
let preview = &mut cx.tab_mut().preview;
|
preview.skip = skip;
|
||||||
if form.upper_bound {
|
}
|
||||||
preview.skip = preview.skip.min(skip);
|
|
||||||
} else {
|
if let Some(index) = form.search_idx {
|
||||||
preview.skip = skip;
|
preview.search_idx = Some(index);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if hovered.is_dir() {
|
if hovered.is_dir() {
|
||||||
|
|
|
||||||
44
yazi-binding/src/elements/highlight.rs
Normal file
44
yazi-binding/src/elements/highlight.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
use mlua::{ExternalError, FromLua, IntoLua, Lua, Result as LuaResult, Value};
|
||||||
|
|
||||||
|
const EXPECTED: &str = "expected a table containing a line and a length";
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct HighlightPosition {
|
||||||
|
pub line: usize,
|
||||||
|
pub col: usize,
|
||||||
|
pub length: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HighlightPosition {
|
||||||
|
pub fn compose(lua: &Lua) -> LuaResult<Value> {
|
||||||
|
let new = lua.create_function(|_, (line, col, length): (usize, usize, usize)| {
|
||||||
|
Ok(Self { line, col, length })
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let tbl = lua.create_table_from([("new", new)])?;
|
||||||
|
tbl.into_lua(lua)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromLua for HighlightPosition {
|
||||||
|
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
|
||||||
|
match value {
|
||||||
|
Value::Table(tbl) => Ok(Self {
|
||||||
|
line: tbl.raw_get("line")?,
|
||||||
|
col: tbl.raw_get("col")?,
|
||||||
|
length: tbl.raw_get("length")?,
|
||||||
|
}),
|
||||||
|
_ => Err(EXPECTED.into_lua_err()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoLua for HighlightPosition {
|
||||||
|
fn into_lua(self, lua: &Lua) -> LuaResult<Value> {
|
||||||
|
let tbl = lua.create_table()?;
|
||||||
|
tbl.set("line", self.line)?;
|
||||||
|
tbl.set("col", self.col)?;
|
||||||
|
tbl.set("length", self.length)?;
|
||||||
|
Ok(Value::Table(tbl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1 +1 @@
|
||||||
yazi_macro::mod_flat!(align area bar border cell color constraint edge fill gauge layout line list pad rect row span spatial table text wrap);
|
yazi_macro::mod_flat!(align area bar border cell color constraint edge fill gauge highlight layout line list pad rect row span spatial table text wrap);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use std::{io::{BufRead, BufReader, Cursor, Seek}, path::PathBuf, sync::OnceLock}
|
||||||
use anyhow::{Result, anyhow, bail};
|
use anyhow::{Result, anyhow, bail};
|
||||||
use ratatui_core::{layout::Size, text::{Line, Span, Text}};
|
use ratatui_core::{layout::Size, text::{Line, Span, Text}};
|
||||||
use syntect::{LoadingError, dumps, easy::HighlightLines, highlighting::{self, Theme, ThemeSet}, parsing::{SyntaxReference, SyntaxSet}};
|
use syntect::{LoadingError, dumps, easy::HighlightLines, highlighting::{self, Theme, ThemeSet}, parsing::{SyntaxReference, SyntaxSet}};
|
||||||
|
use yazi_binding::elements::HighlightPosition;
|
||||||
use yazi_config::{THEME, YAZI};
|
use yazi_config::{THEME, YAZI};
|
||||||
use yazi_runner::previewer::PeekError;
|
use yazi_runner::previewer::PeekError;
|
||||||
use yazi_shared::{id::{Id, Ids}, replace_to_printable};
|
use yazi_shared::{id::{Id, Ids}, replace_to_printable};
|
||||||
|
|
@ -25,12 +26,17 @@ pub struct Highlighter {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Highlighter {
|
impl Highlighter {
|
||||||
pub async fn oneshot<P>(path: P, skip: usize, size: Size) -> Result<Text<'static>, PeekError>
|
pub async fn oneshot<P>(
|
||||||
|
path: P,
|
||||||
|
skip: usize,
|
||||||
|
size: Size,
|
||||||
|
position: Option<HighlightPosition>,
|
||||||
|
) -> Result<Text<'static>, PeekError>
|
||||||
where
|
where
|
||||||
P: Into<PathBuf>,
|
P: Into<PathBuf>,
|
||||||
{
|
{
|
||||||
let path = path.into();
|
let path = path.into();
|
||||||
tokio::task::spawn_blocking(move || Self::make(path, skip, size)?.highlight()).await?
|
tokio::task::spawn_blocking(move || Self::make(path, skip, size)?.highlight(position)).await?
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make<P>(path: P, skip: usize, size: Size) -> Result<Self>
|
fn make<P>(path: P, skip: usize, size: Size) -> Result<Self>
|
||||||
|
|
@ -59,7 +65,7 @@ impl Highlighter {
|
||||||
|
|
||||||
pub fn abort() { INCR.next(); }
|
pub fn abort() { INCR.next(); }
|
||||||
|
|
||||||
fn highlight(mut self) -> Result<Text<'static>, PeekError> {
|
fn highlight(mut self, position: Option<HighlightPosition>) -> Result<Text<'static>, PeekError> {
|
||||||
self.load_syntax()?;
|
self.load_syntax()?;
|
||||||
let mut plain = self.syntax.is_none();
|
let mut plain = self.syntax.is_none();
|
||||||
|
|
||||||
|
|
@ -78,9 +84,9 @@ impl Highlighter {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.ensure_not_cancelled()?;
|
self.ensure_not_cancelled()?;
|
||||||
if plain && !self.process_plain(&buf, &mut i, &mut lines)? {
|
if plain && !self.process_plain(&buf, &mut i, &position, &mut lines)? {
|
||||||
break;
|
break;
|
||||||
} else if !plain && !self.process_hyper(&buf, &mut i, &mut lines)? {
|
} else if !plain && !self.process_hyper(&buf, &mut i, &position, &mut lines)? {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
buf.clear();
|
buf.clear();
|
||||||
|
|
@ -89,11 +95,16 @@ impl Highlighter {
|
||||||
if self.skip > 0 && i < self.skip + self.size.height as usize {
|
if self.skip > 0 && i < self.skip + self.size.height as usize {
|
||||||
return Err(PeekError::Exceeded(i.saturating_sub(self.size.height as _)));
|
return Err(PeekError::Exceeded(i.saturating_sub(self.size.height as _)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Text::from(lines))
|
Ok(Text::from(lines))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_plain(&mut self, buf: &[u8], i: &mut usize, lines: &mut Vec<Line>) -> Result<bool> {
|
fn process_plain(
|
||||||
|
&mut self,
|
||||||
|
buf: &[u8],
|
||||||
|
i: &mut usize,
|
||||||
|
position: &Option<HighlightPosition>,
|
||||||
|
lines: &mut Vec<Line>,
|
||||||
|
) -> Result<bool> {
|
||||||
let b = replace_to_printable(buf, true, YAZI.preview.tab_size, false);
|
let b = replace_to_printable(buf, true, YAZI.preview.tab_size, false);
|
||||||
let s = String::from_utf8_lossy(&b);
|
let s = String::from_utf8_lossy(&b);
|
||||||
|
|
||||||
|
|
@ -107,20 +118,31 @@ impl Highlighter {
|
||||||
if *i > self.skip + self.size.height as usize {
|
if *i > self.skip + self.size.height as usize {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
} else if *i > self.skip {
|
} else if *i > self.skip {
|
||||||
lines.push(spans.into_static_line());
|
let mut static_line = spans.into_static_line();
|
||||||
|
if let Some(pos) = position
|
||||||
|
&& pos.line == *i
|
||||||
|
{
|
||||||
|
static_line.style = ratatui_core::style::Style::new().bg(ratatui_core::style::Color::Red);
|
||||||
|
}
|
||||||
|
lines.push(static_line);
|
||||||
}
|
}
|
||||||
self.ensure_not_cancelled()?;
|
self.ensure_not_cancelled()?;
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_hyper(&mut self, buf: &[u8], i: &mut usize, lines: &mut Vec<Line>) -> Result<bool> {
|
fn process_hyper(
|
||||||
|
&mut self,
|
||||||
|
buf: &[u8],
|
||||||
|
i: &mut usize,
|
||||||
|
position: &Option<HighlightPosition>,
|
||||||
|
lines: &mut Vec<Line>,
|
||||||
|
) -> Result<bool> {
|
||||||
let Some(syntax) = self.syntax else { bail!("No syntax") };
|
let Some(syntax) = self.syntax else { bail!("No syntax") };
|
||||||
let h = self.inner.get_or_insert_with(|| HighlightLines::new(syntax, self.theme));
|
let h = self.inner.get_or_insert_with(|| HighlightLines::new(syntax, self.theme));
|
||||||
|
|
||||||
let s = String::from_utf8_lossy(buf);
|
let s = String::from_utf8_lossy(buf);
|
||||||
let line = [Self::to_line_widget(h.highlight_line(&s, self.syntaxes)?)];
|
|
||||||
|
|
||||||
|
let line = [Self::to_line_widget(h.highlight_line(&s, self.syntaxes)?)];
|
||||||
let mut it = LineIter::parsed(&line, YAZI.preview.tab_size);
|
let mut it = LineIter::parsed(&line, YAZI.preview.tab_size);
|
||||||
if let Some(wrap) = YAZI.preview.wrap.into() {
|
if let Some(wrap) = YAZI.preview.wrap.into() {
|
||||||
it = it.wrapped(wrap, self.size.width);
|
it = it.wrapped(wrap, self.size.width);
|
||||||
|
|
@ -131,10 +153,17 @@ impl Highlighter {
|
||||||
if *i > self.skip + self.size.height as usize {
|
if *i > self.skip + self.size.height as usize {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
} else if *i > self.skip {
|
} else if *i > self.skip {
|
||||||
lines.push(spans.into_static_line());
|
let mut static_line = spans.into_static_line();
|
||||||
|
if let Some(pos) = position
|
||||||
|
&& pos.line == *i
|
||||||
|
{
|
||||||
|
static_line.style = ratatui_core::style::Style::new().bg(ratatui_core::style::Color::Red);
|
||||||
|
}
|
||||||
|
lines.push(static_line);
|
||||||
}
|
}
|
||||||
self.ensure_not_cancelled()?;
|
self.ensure_not_cancelled()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,9 +48,10 @@ impl MgrProxy {
|
||||||
cha: job.file.cha,
|
cha: job.file.cha,
|
||||||
mime: job.mime,
|
mime: job.mime,
|
||||||
|
|
||||||
skip: job.skip,
|
skip: job.skip,
|
||||||
area: area.into(),
|
search_idx: job.search_idx,
|
||||||
data: vec![
|
area: area.into(),
|
||||||
|
data: vec![
|
||||||
Renderable::Clear(Default::default()).with_area(area),
|
Renderable::Clear(Default::default()).with_area(area),
|
||||||
Renderable::from(Error::custom(error)).with_area(area),
|
Renderable::from(Error::custom(error)).with_area(area),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,11 @@ use yazi_vfs::{VfsEntries, VfsFilesOp};
|
||||||
|
|
||||||
use crate::{AppProxy, Highlighter, MgrProxy, tab::PreviewLock};
|
use crate::{AppProxy, Highlighter, MgrProxy, tab::PreviewLock};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default, Debug)]
|
||||||
pub struct Preview {
|
pub struct Preview {
|
||||||
pub lock: Option<PreviewLock>,
|
pub lock: Option<PreviewLock>,
|
||||||
pub skip: usize,
|
pub skip: usize,
|
||||||
|
pub search_idx: Option<usize>,
|
||||||
|
|
||||||
handle: Option<JoinHandle<()>>,
|
handle: Option<JoinHandle<()>>,
|
||||||
pub folder_lock: Option<UrlBuf>,
|
pub folder_lock: Option<UrlBuf>,
|
||||||
|
|
@ -35,7 +36,8 @@ impl Preview {
|
||||||
};
|
};
|
||||||
|
|
||||||
self.abort();
|
self.abort();
|
||||||
let job = PeekJob { previewer, file, mime, skip: self.skip };
|
|
||||||
|
let job = PeekJob { previewer, file, mime, skip: self.skip, search_idx: self.search_idx };
|
||||||
|
|
||||||
self.handle = Some(tokio::spawn(async move {
|
self.handle = Some(tokio::spawn(async move {
|
||||||
let mut rx = RUNNER.peek(&job).await;
|
let mut rx = RUNNER.peek(&job).await;
|
||||||
|
|
@ -83,6 +85,7 @@ impl Preview {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
|
self.search_idx = None;
|
||||||
self.abort();
|
self.abort();
|
||||||
ADAPTOR.image_hide().ok();
|
ADAPTOR.image_hide().ok();
|
||||||
render!(self.lock.take().is_some())
|
render!(self.lock.take().is_some())
|
||||||
|
|
@ -101,7 +104,8 @@ impl Preview {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn same_lock(&self, file: &File, mime: &str) -> bool {
|
pub fn same_lock(&self, file: &File, mime: &str) -> bool {
|
||||||
self.same_file(file, mime) && matches!(&self.lock, Some(l) if l.skip == self.skip)
|
self.same_file(file, mime)
|
||||||
|
&& matches!(&self.lock, Some(l) if l.skip == self.skip && l.search_idx == self.search_idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn same_folder(&self, url: &UrlBuf) -> bool { self.folder_lock.as_ref() == Some(url) }
|
pub fn same_folder(&self, url: &UrlBuf) -> bool { self.folder_lock.as_ref() == Some(url) }
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,10 @@ pub struct PreviewLock {
|
||||||
pub cha: Cha,
|
pub cha: Cha,
|
||||||
pub mime: Symbol<str>,
|
pub mime: Symbol<str>,
|
||||||
|
|
||||||
pub skip: usize,
|
pub skip: usize,
|
||||||
pub area: Rect,
|
pub search_idx: Option<usize>,
|
||||||
pub data: Vec<Renderable>,
|
pub area: Rect,
|
||||||
|
pub data: Vec<Renderable>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_data_any!(PreviewLock);
|
impl_data_any!(PreviewLock);
|
||||||
|
|
@ -29,9 +30,10 @@ impl TryFrom<Table> for PreviewLock {
|
||||||
cha: f.cha,
|
cha: f.cha,
|
||||||
mime: t.raw_get::<LuaString>("mime")?.to_str()?.intern(),
|
mime: t.raw_get::<LuaString>("mime")?.to_str()?.intern(),
|
||||||
|
|
||||||
skip: t.raw_get("skip")?,
|
skip: t.raw_get("skip")?,
|
||||||
area: t.raw_get("area")?,
|
search_idx: t.raw_get("search_idx")?,
|
||||||
data: Default::default(),
|
area: t.raw_get("area")?,
|
||||||
|
data: Default::default(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ pub struct PeekForm {
|
||||||
pub force: bool,
|
pub force: bool,
|
||||||
pub only_if: Option<UrlBuf>,
|
pub only_if: Option<UrlBuf>,
|
||||||
pub upper_bound: bool,
|
pub upper_bound: bool,
|
||||||
|
pub search_idx: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ActionCow> for PeekForm {
|
impl From<ActionCow> for PeekForm {
|
||||||
|
|
@ -16,6 +17,7 @@ impl From<ActionCow> for PeekForm {
|
||||||
force: a.bool("force"),
|
force: a.bool("force"),
|
||||||
only_if: a.take("only-if").ok(),
|
only_if: a.take("only-if").ok(),
|
||||||
upper_bound: a.bool("upper-bound"),
|
upper_bound: a.bool("upper-bound"),
|
||||||
|
search_idx: a.take("search-idx").ok(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,104 @@
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
local function get_search_occurrences(url)
|
||||||
|
if not url.is_search then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
-- Shoud we encode ~ too?
|
||||||
|
local subject_len, occurrences = tostring(url):match("^search://([^~]+)~(.-)//")
|
||||||
|
local lines = {}
|
||||||
|
for occurrence in occurrences:gmatch("[^,]+") do
|
||||||
|
local line, col = occurrence:match("(%d+)-(%d+)")
|
||||||
|
table.insert(lines, { tonumber(line), tonumber(col) })
|
||||||
|
end
|
||||||
|
|
||||||
|
if #lines == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
return tonumber(subject_len), lines
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_next_occurrence_idx(search_idx, direction, occurrences_len)
|
||||||
|
local index
|
||||||
|
|
||||||
|
if direction == "up" then
|
||||||
|
index = (search_idx or 1) - 1
|
||||||
|
if index < 1 then
|
||||||
|
index = occurrences_len
|
||||||
|
end
|
||||||
|
else
|
||||||
|
index = (search_idx or 1) + 1
|
||||||
|
if index > occurrences_len then
|
||||||
|
index = 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return index
|
||||||
|
end
|
||||||
|
|
||||||
function M:peek(job)
|
function M:peek(job)
|
||||||
|
local subject_len, search_occurrences = get_search_occurrences(job.file.url)
|
||||||
|
local search_idx = job.search_idx
|
||||||
|
|
||||||
|
if search_idx == nil and search_occurrences then
|
||||||
|
local occurrence = search_occurrences[1]
|
||||||
|
local line = occurrence[1]
|
||||||
|
local skip = math.max(0, line - 1)
|
||||||
|
ya.emit("peek", {
|
||||||
|
skip,
|
||||||
|
only_if = job.file.url,
|
||||||
|
search_idx = 1,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local occurrence
|
||||||
|
if search_occurrences and search_idx then
|
||||||
|
occurrence = search_occurrences[search_idx]
|
||||||
|
job.position = {
|
||||||
|
line = occurrence[1],
|
||||||
|
col = occurrence[2],
|
||||||
|
length = subject_len,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
local err, bound = ya.preview_code(job)
|
local err, bound = ya.preview_code(job)
|
||||||
if bound then
|
if bound then
|
||||||
ya.emit("peek", { bound, only_if = job.file.url, upper_bound = true })
|
ya.emit("peek", { bound, only_if = job.file.url, upper_bound = true, search_idx = search_idx })
|
||||||
elseif err and not err:find("cancelled", 1, true) then
|
elseif err and not err:find("cancelled", 1, true) then
|
||||||
require("empty").msg(job, err)
|
require("empty").msg(job, err)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function M:seek(job)
|
function M:seek(job)
|
||||||
|
local direction = job.units > 0 and "down" or "up"
|
||||||
|
local search_idx = cx.active.preview.search_idx
|
||||||
|
|
||||||
local h = cx.active.current.hovered
|
local h = cx.active.current.hovered
|
||||||
if not h or h.url ~= job.file.url then
|
if not h or h.url ~= job.file.url then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local _, search_occurrences = get_search_occurrences(job.file.url)
|
||||||
|
if search_occurrences then
|
||||||
|
local next_occurrence_idx = get_next_occurrence_idx(search_idx, direction, #search_occurrences)
|
||||||
|
local occurrence = search_occurrences[next_occurrence_idx]
|
||||||
|
local line = occurrence[1]
|
||||||
|
local skip = math.max(0, line - 1)
|
||||||
|
|
||||||
|
ya.emit("peek", { skip, only_if = job.file.url, search_idx = next_occurrence_idx })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local step = math.floor(job.units * job.area.h / 10)
|
local step = math.floor(job.units * job.area.h / 10)
|
||||||
step = step == 0 and ya.clamp(-1, job.units, 1) or step
|
step = step == 0 and ya.clamp(-1, job.units, 1) or step
|
||||||
|
local skip = math.max(0, cx.active.preview.skip + step)
|
||||||
|
|
||||||
ya.emit("peek", {
|
ya.emit("peek", {
|
||||||
math.max(0, cx.active.preview.skip + step),
|
skip,
|
||||||
only_if = job.file.url,
|
only_if = job.file.url,
|
||||||
|
search_idx = search_idx,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
71
yazi-plugin/src/external/rg.rs
vendored
71
yazi-plugin/src/external/rg.rs
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{path::Path, process::Stdio};
|
use std::process::Stdio;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command, sync::mpsc::{self, UnboundedReceiver}};
|
use tokio::{io::{AsyncBufReadExt, BufReader}, process::Command, sync::mpsc::{self, UnboundedReceiver}};
|
||||||
|
|
@ -15,10 +15,10 @@ pub struct RgOpt {
|
||||||
|
|
||||||
pub fn rg(opt: RgOpt) -> Result<UnboundedReceiver<File>> {
|
pub fn rg(opt: RgOpt) -> Result<UnboundedReceiver<File>> {
|
||||||
let mut child = Command::new("rg")
|
let mut child = Command::new("rg")
|
||||||
.args(["--color=never", "--files-with-matches", "--smart-case"])
|
.args(["--color=never", "--no-heading", "--column", "--smart-case"])
|
||||||
.arg(if opt.hidden { "--hidden" } else { "--no-hidden" })
|
.arg(if opt.hidden { "--hidden" } else { "--no-hidden" })
|
||||||
.args(opt.args)
|
.args(opt.args)
|
||||||
.arg(opt.subject)
|
.arg(opt.subject.clone())
|
||||||
.current_dir(&*opt.cwd.as_url().unified_path())
|
.current_dir(&*opt.cwd.as_url().unified_path())
|
||||||
.kill_on_drop(true)
|
.kill_on_drop(true)
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
|
|
@ -29,18 +29,67 @@ pub fn rg(opt: RgOpt) -> Result<UnboundedReceiver<File>> {
|
||||||
let (tx, rx) = mpsc::unbounded_channel();
|
let (tx, rx) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
while let Ok(Some(line)) = it.next_line().await {
|
let mut current_file: String = String::new();
|
||||||
if Path::new(&line).is_absolute() {
|
let mut current_occurrences: Vec<(u32, u32)> = vec![];
|
||||||
continue;
|
|
||||||
|
while let Ok(Some(search_line)) = it.next_line().await {
|
||||||
|
let Some((file_path, line, col)) = parse_rg_line(&search_line) else { continue };
|
||||||
|
|
||||||
|
if current_file != file_path {
|
||||||
|
let Some(url) =
|
||||||
|
build_file_url(opt.cwd.clone(), opt.subject.clone(), ¤t_file, ¤t_occurrences)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(file) = File::new(url).await {
|
||||||
|
tx.send(file).ok();
|
||||||
|
}
|
||||||
|
current_file = file_path.clone();
|
||||||
|
current_occurrences = vec![];
|
||||||
}
|
}
|
||||||
let Ok(url) = opt.cwd.try_join(line) else {
|
|
||||||
continue;
|
current_occurrences.push((line as u32, col as u32));
|
||||||
};
|
}
|
||||||
if let Ok(file) = File::new(url).await {
|
|
||||||
tx.send(file).ok();
|
if current_occurrences.len() > 0 {
|
||||||
|
match build_file_url(opt.cwd, opt.subject, ¤t_file, ¤t_occurrences) {
|
||||||
|
Some(url) => {
|
||||||
|
if let Ok(file) = File::new(url).await {
|
||||||
|
tx.send(file).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
child.wait().await.ok();
|
child.wait().await.ok();
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(rx)
|
Ok(rx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_file_url(
|
||||||
|
cwd: UrlBuf,
|
||||||
|
subject: String,
|
||||||
|
file_path: &str,
|
||||||
|
occurences: &Vec<(u32, u32)>,
|
||||||
|
) -> Option<UrlBuf> {
|
||||||
|
let occurrences_str =
|
||||||
|
occurences.iter().map(|(line, col)| format!("{line}-{col}")).collect::<Vec<_>>().join(",");
|
||||||
|
let search_str = format!("{}~{}", subject.len(), occurrences_str);
|
||||||
|
let url = cwd.try_join(file_path).ok().and_then(|u| u.into_search(search_str).ok());
|
||||||
|
|
||||||
|
url
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_rg_line(line: &str) -> Option<(String, usize, usize)> {
|
||||||
|
let mut parts = line.split(':');
|
||||||
|
|
||||||
|
let (file, line, col) = (parts.next()?, parts.next()?, parts.next()?);
|
||||||
|
if file.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((file.to_owned(), line.parse().ok()?, col.parse().ok()?))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use mlua::{ExternalError, Function, IntoLuaMulti, Lua, Table, Value};
|
use mlua::{ExternalError, Function, IntoLuaMulti, Lua, Table, Value};
|
||||||
use yazi_binding::{Error, elements::Area};
|
use yazi_binding::{Error, elements::{Area, HighlightPosition}};
|
||||||
use yazi_core::{Highlighter, MgrProxy, tab::PreviewLock};
|
use yazi_core::{Highlighter, MgrProxy, tab::PreviewLock};
|
||||||
use yazi_fs::FsUrl;
|
use yazi_fs::FsUrl;
|
||||||
use yazi_runner::previewer::PeekError;
|
use yazi_runner::previewer::PeekError;
|
||||||
|
|
@ -15,10 +15,12 @@ impl Utils {
|
||||||
pub(super) fn preview_code(lua: &Lua) -> mlua::Result<Function> {
|
pub(super) fn preview_code(lua: &Lua) -> mlua::Result<Function> {
|
||||||
lua.create_async_function(|lua, t: Table| async move {
|
lua.create_async_function(|lua, t: Table| async move {
|
||||||
let area: Area = t.raw_get("area")?;
|
let area: Area = t.raw_get("area")?;
|
||||||
let mut lock = PreviewLock::try_from(t)?;
|
let position: Option<HighlightPosition> = t.raw_get("position").ok();
|
||||||
|
|
||||||
|
let mut lock = PreviewLock::try_from(t)?;
|
||||||
let path = lock.url.as_url().unified_path();
|
let path = lock.url.as_url().unified_path();
|
||||||
let inner = match Highlighter::oneshot(path, lock.skip, area.size()).await {
|
|
||||||
|
let inner = match Highlighter::oneshot(path, lock.skip, area.size(), position).await {
|
||||||
Ok(text) => text,
|
Ok(text) => text,
|
||||||
Err(e @ PeekError::Exceeded(max)) => return (e, max).into_lua_multi(&lua),
|
Err(e @ PeekError::Exceeded(max)) => return (e, max).into_lua_multi(&lua),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -33,6 +35,9 @@ impl Utils {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: You need to implement or update Highlighter::oneshot_with_highlight to
|
||||||
|
// accept line/column and highlight accordingly.
|
||||||
|
|
||||||
pub(super) fn preview_widget(lua: &Lua) -> mlua::Result<Function> {
|
pub(super) fn preview_widget(lua: &Lua) -> mlua::Result<Function> {
|
||||||
lua.create_async_function(|_, (t, value): (Table, Value)| async move {
|
lua.create_async_function(|_, (t, value): (Table, Value)| async move {
|
||||||
let mut lock = PreviewLock::try_from(t)?;
|
let mut lock = PreviewLock::try_from(t)?;
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,11 @@ use yazi_shared::{data::Sendable, pool::Symbol};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct PeekJob {
|
pub struct PeekJob {
|
||||||
pub previewer: PreviewerArc,
|
pub previewer: PreviewerArc,
|
||||||
pub file: File,
|
pub file: File,
|
||||||
pub mime: Symbol<str>,
|
pub mime: Symbol<str>,
|
||||||
pub skip: usize,
|
pub skip: usize,
|
||||||
|
pub search_idx: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoLua for PeekJob {
|
impl IntoLua for PeekJob {
|
||||||
|
|
@ -21,6 +22,7 @@ impl IntoLua for PeekJob {
|
||||||
("file", self.file.into_lua(lua)?),
|
("file", self.file.into_lua(lua)?),
|
||||||
("mime", self.mime.into_lua(lua)?),
|
("mime", self.mime.into_lua(lua)?),
|
||||||
("skip", self.skip.into_lua(lua)?),
|
("skip", self.skip.into_lua(lua)?),
|
||||||
|
("search_idx", self.search_idx.into_lua(lua)?),
|
||||||
])?
|
])?
|
||||||
.into_lua(lua)
|
.into_lua(lua)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue