use std::fmt;
use std::sync::Arc;
use rich::{Renderable, Text};
use crate::event::Key;
#[derive(Clone)]
pub enum Preview {
Text(String),
Markup(String),
Renderable(Arc<dyn Renderable + Send + Sync>),
}
impl Preview {
pub fn renderable(&self) -> Arc<dyn Renderable + Send + Sync> {
match self {
Preview::Text(text) => Arc::new(Text::new(text.clone())),
Preview::Markup(markup) => {
Arc::new(Text::from_markup(markup).unwrap_or_else(|_| Text::new(markup.clone())))
}
Preview::Renderable(renderable) => Arc::clone(renderable),
}
}
}
impl fmt::Debug for Preview {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Preview::Text(text) => f.debug_tuple("Text").field(text).finish(),
Preview::Markup(markup) => f.debug_tuple("Markup").field(markup).finish(),
Preview::Renderable(_) => f.write_str("Renderable(..)"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Action {
pub id: String,
pub label: String,
pub key: Option<Key>,
}
impl Action {
pub fn new(id: impl Into<String>, label: impl Into<String>, key: Key) -> Action {
Action {
id: id.into(),
label: label.into(),
key: Some(key),
}
}
pub fn menu(id: impl Into<String>, label: impl Into<String>) -> Action {
Action {
id: id.into(),
label: label.into(),
key: None,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum TargetKind {
#[default]
Item,
Row,
Node,
File,
Region,
}
impl TargetKind {
pub fn as_str(self) -> &'static str {
match self {
TargetKind::Item => "item",
TargetKind::Row => "row",
TargetKind::Node => "node",
TargetKind::File => "file",
TargetKind::Region => "region",
}
}
pub fn parse(name: &str) -> Option<TargetKind> {
Some(match name {
"item" => TargetKind::Item,
"row" => TargetKind::Row,
"node" => TargetKind::Node,
"file" => TargetKind::File,
"region" => TargetKind::Region,
_ => return None,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ActionTarget {
pub kind: TargetKind,
pub index: usize,
pub label: String,
pub value: String,
}
pub type ActionFilter = Arc<dyn Fn(&ActionTarget) -> bool + Send + Sync>;
#[derive(Clone, Default)]
pub struct Actions {
entries: Vec<(Action, Option<ActionFilter>)>,
}
impl fmt::Debug for Actions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list()
.entries(self.entries.iter().map(|(action, _)| action))
.finish()
}
}
impl Actions {
pub fn new() -> Actions {
Actions::default()
}
pub fn action(mut self, action: Action) -> Actions {
self.entries.push((action, None));
self
}
pub fn action_for(self, kind: TargetKind, action: Action) -> Actions {
self.action_if(action, move |target| target.kind == kind)
}
pub fn action_if(
mut self,
action: Action,
filter: impl Fn(&ActionTarget) -> bool + Send + Sync + 'static,
) -> Actions {
self.entries.push((action, Some(Arc::new(filter))));
self
}
pub fn extend(mut self, other: Actions) -> Actions {
self.entries.extend(other.entries);
self
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn for_target(&self, target: &ActionTarget, own: &[Action]) -> Vec<Action> {
let mut out: Vec<Action> = Vec::new();
let applies = self
.entries
.iter()
.filter(|(_, filter)| filter.as_ref().is_none_or(|filter| filter(target)));
for action in own.iter().chain(applies.map(|(action, _)| action)) {
if !out.iter().any(|known| known.id == action.id) {
out.push(action.clone());
}
}
out
}
pub fn from_registry(registry: &rich_ext::registry::ExtensionRegistry) -> Actions {
let mut actions = Actions::new();
for (name, _, action) in registry.actions() {
let entry = Action {
id: name.to_string(),
label: action.label(),
key: action.key().as_deref().and_then(Key::parse),
};
actions = actions.action_if(entry, move |target| {
action.applies(target.kind.as_str(), &target.value)
});
}
actions
}
}
#[derive(Clone, Debug)]
pub struct Item<T> {
pub value: T,
pub label: String,
pub description: Option<String>,
pub metadata: Vec<(String, String)>,
pub preview: Option<Preview>,
pub actions: Vec<Action>,
pub keywords: Vec<String>,
}
impl<T> Item<T> {
pub fn new(value: T, label: impl Into<String>) -> Item<T> {
Item {
value,
label: label.into(),
description: None,
metadata: Vec::new(),
preview: None,
actions: Vec::new(),
keywords: Vec::new(),
}
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.push((key.into(), value.into()));
self
}
pub fn preview(mut self, preview: Preview) -> Self {
self.preview = Some(preview);
self
}
pub fn action(mut self, action: Action) -> Self {
self.actions.push(action);
self
}
pub fn keyword(mut self, keyword: impl Into<String>) -> Self {
self.keywords.push(keyword.into());
self
}
pub fn search_text(&self) -> String {
let mut text = self.label.clone();
for field in self
.description
.iter()
.chain(self.metadata.iter().map(|(_, value)| value))
.chain(&self.keywords)
{
text.push('\n');
text.push_str(field);
}
text
}
pub fn action_for(&self, key: Key) -> Option<&Action> {
self.actions.iter().find(|action| action.key == Some(key))
}
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Item<U> {
Item {
value: f(self.value),
label: self.label,
description: self.description,
metadata: self.metadata,
preview: self.preview,
actions: self.actions,
keywords: self.keywords,
}
}
}
impl<T: fmt::Display> From<T> for Item<T> {
fn from(value: T) -> Item<T> {
let label = value.to_string();
Item::new(value, label)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builds_and_searches_items() {
let item = Item::new(std::path::PathBuf::from("src/main.rs"), "main.rs")
.description("the entry point")
.meta("size", "2 KB")
.keyword("src/main.rs")
.preview(Preview::Text("fn main() {}".into()))
.action(Action::new("edit", "Open in $EDITOR", Key::ctrl('e')));
assert_eq!(
item.search_text(),
"main.rs\nthe entry point\n2 KB\nsrc/main.rs"
);
assert_eq!(item.action_for(Key::ctrl('e')).unwrap().id, "edit");
assert!(item.action_for(Key::ctrl('d')).is_none());
let named = item.map(|path| path.display().to_string());
assert_eq!(named.value, "src/main.rs");
assert_eq!(Item::from(42).label, "42");
}
fn target(kind: TargetKind, value: &str) -> ActionTarget {
ActionTarget {
kind,
index: 0,
label: value.into(),
value: value.into(),
}
}
#[test]
fn view_actions_apply_by_target() {
let actions = Actions::new()
.action(Action::menu("copy", "Copy"))
.action_for(
TargetKind::File,
Action::new("edit", "Edit", Key::ctrl('e')),
)
.action_if(Action::menu("rust", "Rust only"), |target| {
target.value.ends_with(".rs")
});
let own = [Action::new("open", "Open", Key::ctrl('o'))];
let ids = |target: &ActionTarget| -> Vec<String> {
actions
.for_target(target, &own)
.into_iter()
.map(|action| action.id)
.collect()
};
assert_eq!(
ids(&target(TargetKind::File, "main.rs")),
["open", "copy", "edit", "rust"]
);
assert_eq!(ids(&target(TargetKind::Row, "a\tb")), ["open", "copy"]);
let shadow = [Action::menu("copy", "Copy path")];
let found = actions.for_target(&target(TargetKind::Item, "x"), &shadow);
assert_eq!(found[0].label, "Copy path");
assert_eq!(found.len(), 1);
assert_eq!(TargetKind::parse("node"), Some(TargetKind::Node));
assert_eq!(TargetKind::Node.as_str(), "node");
}
#[test]
fn plugin_actions_come_from_the_registry() {
use rich_plugin_api::{CustomAction, Plugin, PluginError, PluginMetadata, PluginRegistrar};
struct Reveal;
impl CustomAction for Reveal {
fn label(&self) -> String {
"Reveal".into()
}
fn key(&self) -> Option<String> {
Some("ctrl+r".into())
}
fn applies(&self, kind: &str, value: &str) -> bool {
kind == "file" && !value.starts_with('.')
}
}
struct Files;
impl Plugin for Files {
fn metadata(&self) -> PluginMetadata {
PluginMetadata::new("files", "Files", "0.0.0")
}
fn register(&self, registrar: &mut dyn PluginRegistrar) -> Result<(), PluginError> {
registrar.action("reveal", Arc::new(Reveal));
Ok(())
}
}
let mut registry = rich_ext::registry::ExtensionRegistry::new();
registry.add_plugin(&Files).unwrap();
let actions = Actions::from_registry(®istry);
let found = actions.for_target(&target(TargetKind::File, "a.txt"), &[]);
assert_eq!(found, [Action::new("reveal", "Reveal", Key::ctrl('r'))]);
assert!(actions
.for_target(&target(TargetKind::File, ".env"), &[])
.is_empty());
assert!(actions
.for_target(&target(TargetKind::Item, "a.txt"), &[])
.is_empty());
}
}