diff --git a/app/src/app.rs b/app/src/app.rs index be4dff505..0ed1cf378 100644 --- a/app/src/app.rs +++ b/app/src/app.rs @@ -76,7 +76,7 @@ impl App { fn dispatch_render(&mut self) { if let Some(term) = &mut self.term { let _ = term.draw(|f| { - plugin::scope(&self.cx, || { + plugin::scope(&self.cx, |_| { f.render_widget(Root::new(&self.cx), f.size()); }); diff --git a/app/src/manager/folder.rs b/app/src/manager/folder.rs index aba92a69e..901186b83 100644 --- a/app/src/manager/folder.rs +++ b/app/src/manager/folder.rs @@ -16,36 +16,14 @@ impl Folder { } impl Folder { - // 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 Widget for Folder { fn render(self, area: Rect, buf: &mut Buffer) { - let x = plugin::Folder { kind: self.kind as u8 }.render(area); + let x = plugin::Folder { kind: self.kind as u8 }.render(area, buf); if x.is_err() { info!("{:?}", x); - return; - } - - for x in x.unwrap() { - x.render(buf); } // let items: Vec<_> = window diff --git a/app/src/status/layout.rs b/app/src/status/layout.rs index d6ecd6103..d31ed5e17 100644 --- a/app/src/status/layout.rs +++ b/app/src/status/layout.rs @@ -5,14 +5,10 @@ pub(crate) struct Layout; impl Widget for Layout { fn render(self, area: Rect, buf: &mut Buffer) { - let x = plugin::Status::render(area); + let x = plugin::Status.render(area, buf); if x.is_err() { info!("{:?}", x); return; } - - for x in x.unwrap() { - x.render(buf); - } } } diff --git a/core/src/manager/finder.rs b/core/src/manager/finder.rs index ab1f4e998..8a7741fe3 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::{collections::BTreeMap, ffi::OsStr, ops::Range}; use anyhow::Result; use regex::bytes::{Regex, RegexBuilder}; @@ -91,13 +91,17 @@ impl Finder { /// Explode the name into three parts: head, body, tail. #[inline] - pub fn explode(&self, name: &[u8]) -> Option<(String, String, String)> { - let range = self.query.find(name).map(|m| m.range())?; - Some(( - String::from_utf8_lossy(&name[..range.start]).to_string(), - String::from_utf8_lossy(&name[range.start..range.end]).to_string(), - String::from_utf8_lossy(&name[range.end..]).to_string(), - )) + pub fn highlighted(&self, name: &OsStr) -> Vec> { + #[cfg(target_os = "windows")] + let found = self.query.find(name.to_string_lossy().as_bytes()); + + #[cfg(not(target_os = "windows"))] + let found = { + use std::os::unix::ffi::OsStrExt; + self.query.find(name.as_bytes()) + }; + + found.map(|m| vec![m.range()]).unwrap_or_default() } } diff --git a/plugin/preset/components/folder.lua b/plugin/preset/components/folder.lua index 1f432272c..0aca21850 100644 --- a/plugin/preset/components/folder.lua +++ b/plugin/preset/components/folder.lua @@ -16,10 +16,6 @@ function Folder:by_kind(kind) end end -function Folder:window(kind) return (self:by_kind(kind) or {}).window end - -function Folder:hovered(kind) return (self:by_kind(kind) or {}).hovered end - function Folder:markers(area, markers) if #markers == 0 then return {} @@ -51,50 +47,47 @@ function Folder:markers(area, markers) end function Folder:parent(area) - local window = self:window(self.Kind.Parent) - if window == nil then + local folder = self:by_kind(self.Kind.Parent) + if folder == nil then return {} end - local hovered = (self:hovered(self.Kind.Parent) or {}).url - local lines = {} - for _, f in ipairs(window) do - local line = ui.Line { ui.Span(" " .. f:icon() .. " " .. f.name .. " ") } - if f.url == hovered then - line = line:style(THEME.files.hovered) + local items = {} + for _, f in ipairs(folder.window) do + local item = ui.ListItem(" " .. f:icon() .. " " .. f.name .. " ") + if f.hovered then + item = item:style(THEME.files.hovered) else - line = line:style(f:style()) + item = item:style(f:style()) end - lines[#lines + 1] = line + items[#items + 1] = item end - return { ui.Paragraph(area, lines) } + return { ui.List(area, items) } end function Folder:current(area) - local hovered = (self:hovered(self.Kind.Current) or {}).url local markers = {} - local lines = {} - for i, f in ipairs(self:window(self.Kind.Current)) do - local name = f.name + local items = {} + for i, f in ipairs(self:by_kind(self.Kind.Current).window) do + local name = ui.highlight_ranges(f.name, f:highlights()) -- Show symlink target if MANAGER.show_symlink then - local link_to = f.link_to - if link_to ~= nil then - name = name .. " -> " .. tostring(link_to) + if f.link_to ~= nil then + name[#name + 1] = ui.Span(" -> " .. tostring(f.link_to)):italic() end end -- Highlight hovered file - local line = ui.Line { ui.Span(" " .. f:icon() .. " " .. name .. " ") } - if f.url == hovered then - line = line:style(THEME.files.hovered) + local item = ui.ListItem(ui.Line { ui.Span(" " .. f:icon() .. " "), table.unpack(name) }) + if f.hovered then + item = item:style(THEME.files.hovered) else - line = line:style(f:style()) + item = item:style(f:style()) end - lines[#lines + 1] = line + items[#items + 1] = item -- Mark selected/yanked files if f:selected() then @@ -102,28 +95,27 @@ function Folder:current(area) end end - return { ui.Paragraph(area, lines), table.unpack(self:markers(area, markers)) } + return { ui.List(area, items), table.unpack(self:markers(area, markers)) } end function Folder:preview(area) - local window = self:window(self.Kind.Preview) - if window == nil then + local folder = self:by_kind(self.Kind.Preview) + if folder == nil then return {} end - local hovered = (self:hovered(self.Kind.Preview) or {}).url - local lines = {} - for _, f in ipairs(window) do - local line = ui.Line { ui.Span(" " .. f:icon() .. " " .. f.name .. " ") } - if f.url == hovered then - line = line:style(THEME.preview.hovered) + local items = {} + for _, f in ipairs(folder.window) do + local item = ui.ListItem(" " .. f:icon() .. " " .. f.name .. " ") + if f.hovered then + item = item:style(THEME.preview.hovered) else - line = line:style(f:style()) + item = item:style(f:style()) end - lines[#lines + 1] = line + items[#items + 1] = item end - return { ui.Paragraph(area, lines) } + return { ui.List(area, items) } end function Folder:render(area, args) diff --git a/plugin/preset/ui.lua b/plugin/preset/ui.lua index 700688943..5b4ccf7bb 100644 --- a/plugin/preset/ui.lua +++ b/plugin/preset/ui.lua @@ -9,3 +9,24 @@ ui = { VERTICAL = true, }, } + +function ui.highlight_ranges(s, ranges) + if ranges == nil or #ranges == 0 then + return { ui.Span(s) } + end + + local spans = {} + local last = 0 + for _, r in ipairs(ranges) do + if r[1] > last then + spans[#spans + 1] = ui.Span(s:sub(last + 1, r[1])) + end + -- TODO: use a customable style + spans[#spans + 1] = ui.Span(s:sub(r[1] + 1, r[2])):fg("yellow"):italic() + last = r[2] + end + if last < #s then + spans[#spans + 1] = ui.Span(s:sub(last + 1)) + end + return spans +end diff --git a/plugin/src/bindings/shared.rs b/plugin/src/bindings/shared.rs index 2ce27a4d1..5a02eff33 100644 --- a/plugin/src/bindings/shared.rs +++ b/plugin/src/bindings/shared.rs @@ -1,5 +1,23 @@ -use mlua::{MetaMethod, UserData, UserDataRef}; +use mlua::{IntoLua, MetaMethod, UserData, UserDataRef}; +// --- Range +pub struct Range(std::ops::Range); + +impl From> for Range { + fn from(value: std::ops::Range) -> Self { Self(value) } +} + +impl<'lua, T> IntoLua<'lua> for Range +where + T: IntoLua<'lua>, +{ + fn into_lua(self, lua: &'lua mlua::Lua) -> mlua::Result { + let tbl = lua.create_sequence_from([self.0.start, self.0.end])?; + tbl.into_lua(lua) + } +} + +// --- Url pub struct Url(shared::Url); impl From<&shared::Url> for Url { diff --git a/plugin/src/bindings/tab.rs b/plugin/src/bindings/tab.rs index c873ec0ab..278c36482 100644 --- a/plugin/src/bindings/tab.rs +++ b/plugin/src/bindings/tab.rs @@ -3,7 +3,7 @@ use core::Ctx; use config::{MANAGER, THEME}; use mlua::{AnyUserData, Function, IntoLua, MetaMethod, UserData, UserDataFields, UserDataMethods, Value}; -use super::Url; +use super::{Range, Url}; use crate::{layout::Style, LUA}; struct File(core::files::File); @@ -89,9 +89,13 @@ impl<'a, 'b> Tab<'a, 'b> { reg.add_function("style", |_, me: AnyUserData| { me.named_user_value::("style")?.call::<_, Style>(()) }); + reg.add_field_function_get("hovered", |_, me| me.named_user_value::("hovered")); reg.add_function("selected", |_, me: AnyUserData| { me.named_user_value::("selected")?.call::<_, bool>(me) }); + reg.add_function("highlights", |_, me: AnyUserData| { + me.named_user_value::("highlights")?.call::<_, Value>(()) + }); reg.add_field_method_get("url", |_, me| Ok(Url::from(me.url()))); reg.add_field_method_get("length", |_, me| Ok(me.length())); @@ -203,6 +207,11 @@ impl<'a, 'b> Tab<'a, 'b> { })?, )?; + ud.set_named_user_value( + "hovered", + matches!(&folder.hovered, Some(f) if f.url() == inner.url()), + )?; + ud.set_named_user_value( "selected", self.scope.create_function(|_, me: AnyUserData| { @@ -217,6 +226,20 @@ impl<'a, 'b> Tab<'a, 'b> { })?, )?; + ud.set_named_user_value( + "highlights", + self.scope.create_function(|_, ()| { + let Some(finder) = self.inner.finder() else { + return Ok(None); + }; + Ok( + inner + .name() + .map(|n| finder.highlighted(n).into_iter().map(Range::from).collect::>()), + ) + })?, + )?; + Ok(ud) } diff --git a/plugin/src/components.rs b/plugin/src/components.rs index 90cd5ef98..0d9c25c1d 100644 --- a/plugin/src/components.rs +++ b/plugin/src/components.rs @@ -1,30 +1,56 @@ -use mlua::{Result, Table, TableExt}; -use ratatui::layout; +use mlua::{AnyUserData, Table, TableExt}; -use crate::{layout::{Paragraph, Rect}, GLOBALS, LUA}; +use crate::{layout::{List, Paragraph, Rect}, GLOBALS, LUA}; +#[inline] +fn layout(values: Vec, buf: &mut ratatui::prelude::Buffer) -> mlua::Result<()> { + for value in values { + if let Ok(c) = value.take::() { + c.render(buf) + } else if let Ok(c) = value.take::() { + c.render(buf) + } + } + Ok(()) +} + +// --- Status pub struct Status; impl Status { - pub fn render(area: layout::Rect) -> Result> { + pub fn render( + self, + area: ratatui::layout::Rect, + buf: &mut ratatui::prelude::Buffer, + ) -> mlua::Result<()> { let comp: Table = GLOBALS.get("Status")?; - comp.call_method::<_, _>("render", Rect(area)) + let values: Vec = comp.call_method::<_, _>("render", Rect(area))?; + + layout(values, buf) } } +// --- Folder pub struct Folder { pub kind: u8, } impl Folder { - fn args(&self) -> Result { + fn args(&self) -> mlua::Result
{ let tbl = LUA.create_table()?; tbl.set("kind", self.kind)?; Ok(tbl) } - pub fn render(self, area: layout::Rect) -> Result> { + pub fn render( + self, + area: ratatui::layout::Rect, + buf: &mut ratatui::prelude::Buffer, + ) -> mlua::Result<()> { let comp: Table = GLOBALS.get("Folder")?; - comp.call_method::<_, _>("render", (Rect(area), self.args()?)) + let values: Vec = + comp.call_method::<_, _>("render", (Rect(area), self.args()?))?; + + layout(values, buf) } } diff --git a/plugin/src/layout/list.rs b/plugin/src/layout/list.rs new file mode 100644 index 000000000..76eb346e7 --- /dev/null +++ b/plugin/src/layout/list.rs @@ -0,0 +1,117 @@ +use mlua::{AnyUserData, FromLua, Lua, Table, UserData, Value}; +use ratatui::widgets::Widget; + +use super::{Line, Rect, Span, Style}; +use crate::{GLOBALS, LUA}; + +// --- List +#[derive(Clone)] +pub(crate) struct List { + area: ratatui::layout::Rect, + + inner: ratatui::widgets::List<'static>, +} + +impl List { + pub(crate) fn install() -> mlua::Result<()> { + let ui: Table = GLOBALS.get("ui")?; + ui.set( + "List", + LUA.create_function(|_, (area, items): (Rect, Vec)| { + let items: Vec<_> = items.into_iter().map(|x| x.into()).collect(); + Ok(Self { area: area.0, inner: ratatui::widgets::List::new(items) }) + })?, + ) + } + + pub(crate) fn render(self, buf: &mut ratatui::buffer::Buffer) { + self.inner.render(self.area, buf); + } +} + +impl<'lua> FromLua<'lua> for List { + fn from_lua(value: Value<'lua>, _: &'lua Lua) -> mlua::Result { + match value { + Value::UserData(ud) => Ok(ud.borrow::()?.clone()), + _ => Err(mlua::Error::FromLuaConversionError { + from: value.type_name(), + to: "List", + message: Some("expected a List".to_string()), + }), + } + } +} + +impl UserData for List {} + +// --- ListItem +#[derive(Clone)] +pub(crate) struct ListItem { + content: ratatui::text::Text<'static>, + style: Option, +} + +impl ListItem { + pub(crate) fn install() -> mlua::Result<()> { + let ui: Table = GLOBALS.get("ui")?; + ui.set( + "ListItem", + LUA.create_function(|_, value: Value| { + match value { + Value::UserData(ud) => { + let content: ratatui::text::Text = if let Ok(line) = ud.take::() { + line.0.into() + } else if let Ok(span) = ud.take::() { + span.0.into() + } else { + return Err(mlua::Error::external("expected a String, Line or Span")); + }; + return Ok(Self { content, style: None }); + } + Value::String(s) => { + return Ok(Self { content: s.to_str()?.to_string().into(), style: None }); + } + _ => {} + } + Err(mlua::Error::external("expected a String, Line or Span")) + })?, + ) + } +} + +impl From for ratatui::widgets::ListItem<'static> { + fn from(value: ListItem) -> Self { + let mut item = Self::new(value.content); + if let Some(style) = value.style { + item = item.style(style) + } + item + } +} + +impl<'lua> FromLua<'lua> for ListItem { + fn from_lua(value: Value<'lua>, _: &'lua Lua) -> mlua::Result { + match value { + Value::UserData(ud) => Ok(ud.borrow::()?.clone()), + _ => Err(mlua::Error::FromLuaConversionError { + from: value.type_name(), + to: "ListItem", + message: Some("expected a ListItem".to_string()), + }), + } + } +} + +impl UserData for ListItem { + fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_function("style", |_, (ud, value): (AnyUserData, Value)| { + ud.borrow_mut::()?.style = match value { + Value::Nil => None, + Value::Table(tbl) => Some(Style::from(tbl).0), + Value::UserData(ud) => Some(ud.borrow::