1use std::fmt;
16use std::sync::Arc;
17
18use rich::{Renderable, Text};
19
20use crate::event::Key;
21
22#[derive(Clone)]
24pub enum Preview {
25 Text(String),
27 Markup(String),
29 Renderable(Arc<dyn Renderable + Send + Sync>),
31}
32
33impl Preview {
34 pub fn renderable(&self) -> Arc<dyn Renderable + Send + Sync> {
36 match self {
37 Preview::Text(text) => Arc::new(Text::new(text.clone())),
38 Preview::Markup(markup) => {
39 Arc::new(Text::from_markup(markup).unwrap_or_else(|_| Text::new(markup.clone())))
40 }
41 Preview::Renderable(renderable) => Arc::clone(renderable),
42 }
43 }
44}
45
46impl fmt::Debug for Preview {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 match self {
49 Preview::Text(text) => f.debug_tuple("Text").field(text).finish(),
50 Preview::Markup(markup) => f.debug_tuple("Markup").field(markup).finish(),
51 Preview::Renderable(_) => f.write_str("Renderable(..)"),
52 }
53 }
54}
55
56#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct Action {
61 pub id: String,
63 pub label: String,
65 pub key: Option<Key>,
66}
67
68impl Action {
69 pub fn new(id: impl Into<String>, label: impl Into<String>, key: Key) -> Action {
70 Action {
71 id: id.into(),
72 label: label.into(),
73 key: Some(key),
74 }
75 }
76
77 pub fn menu(id: impl Into<String>, label: impl Into<String>) -> Action {
79 Action {
80 id: id.into(),
81 label: label.into(),
82 key: None,
83 }
84 }
85}
86
87#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
89pub enum TargetKind {
90 #[default]
92 Item,
93 Row,
95 Node,
97 File,
99 Region,
103}
104
105impl TargetKind {
106 pub fn as_str(self) -> &'static str {
108 match self {
109 TargetKind::Item => "item",
110 TargetKind::Row => "row",
111 TargetKind::Node => "node",
112 TargetKind::File => "file",
113 TargetKind::Region => "region",
114 }
115 }
116
117 pub fn parse(name: &str) -> Option<TargetKind> {
118 Some(match name {
119 "item" => TargetKind::Item,
120 "row" => TargetKind::Row,
121 "node" => TargetKind::Node,
122 "file" => TargetKind::File,
123 "region" => TargetKind::Region,
124 _ => return None,
125 })
126 }
127}
128
129#[derive(Clone, Debug, PartialEq, Eq)]
131pub struct ActionTarget {
132 pub kind: TargetKind,
133 pub index: usize,
135 pub label: String,
137 pub value: String,
141}
142
143pub type ActionFilter = Arc<dyn Fn(&ActionTarget) -> bool + Send + Sync>;
145
146#[derive(Clone, Default)]
150pub struct Actions {
151 entries: Vec<(Action, Option<ActionFilter>)>,
152}
153
154impl fmt::Debug for Actions {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 f.debug_list()
157 .entries(self.entries.iter().map(|(action, _)| action))
158 .finish()
159 }
160}
161
162impl Actions {
163 pub fn new() -> Actions {
164 Actions::default()
165 }
166
167 #[must_use]
169 pub fn action(mut self, action: Action) -> Actions {
170 self.entries.push((action, None));
171 self
172 }
173
174 #[must_use]
176 pub fn action_for(self, kind: TargetKind, action: Action) -> Actions {
177 self.action_if(action, move |target| target.kind == kind)
178 }
179
180 #[must_use]
182 pub fn action_if(
183 mut self,
184 action: Action,
185 filter: impl Fn(&ActionTarget) -> bool + Send + Sync + 'static,
186 ) -> Actions {
187 self.entries.push((action, Some(Arc::new(filter))));
188 self
189 }
190
191 #[must_use]
193 pub fn extend(mut self, other: Actions) -> Actions {
194 self.entries.extend(other.entries);
195 self
196 }
197
198 pub fn is_empty(&self) -> bool {
199 self.entries.is_empty()
200 }
201
202 pub fn len(&self) -> usize {
203 self.entries.len()
204 }
205
206 pub fn for_target(&self, target: &ActionTarget, own: &[Action]) -> Vec<Action> {
209 let mut out: Vec<Action> = Vec::new();
210 let applies = self
211 .entries
212 .iter()
213 .filter(|(_, filter)| filter.as_ref().is_none_or(|filter| filter(target)));
214 for action in own.iter().chain(applies.map(|(action, _)| action)) {
215 if !out.iter().any(|known| known.id == action.id) {
216 out.push(action.clone());
217 }
218 }
219 out
220 }
221
222 pub fn from_registry(registry: &rich_ext::registry::ExtensionRegistry) -> Actions {
227 let mut actions = Actions::new();
228 for (name, _, action) in registry.actions() {
229 let entry = Action {
230 id: name.to_string(),
231 label: action.label(),
232 key: action.key().as_deref().and_then(Key::parse),
233 };
234 actions = actions.action_if(entry, move |target| {
235 action.applies(target.kind.as_str(), &target.value)
236 });
237 }
238 actions
239 }
240}
241
242#[derive(Clone, Debug)]
244#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
245pub struct Item<T> {
246 pub value: T,
247 pub label: String,
249 pub description: Option<String>,
251 pub metadata: Vec<(String, String)>,
253 pub preview: Option<Preview>,
254 pub actions: Vec<Action>,
255 pub keywords: Vec<String>,
258}
259
260impl<T> Item<T> {
261 pub fn new(value: T, label: impl Into<String>) -> Item<T> {
262 Item {
263 value,
264 label: label.into(),
265 description: None,
266 metadata: Vec::new(),
267 preview: None,
268 actions: Vec::new(),
269 keywords: Vec::new(),
270 }
271 }
272
273 pub fn description(mut self, description: impl Into<String>) -> Self {
274 self.description = Some(description.into());
275 self
276 }
277
278 pub fn meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
279 self.metadata.push((key.into(), value.into()));
280 self
281 }
282
283 pub fn preview(mut self, preview: Preview) -> Self {
284 self.preview = Some(preview);
285 self
286 }
287
288 pub fn action(mut self, action: Action) -> Self {
289 self.actions.push(action);
290 self
291 }
292
293 pub fn keyword(mut self, keyword: impl Into<String>) -> Self {
294 self.keywords.push(keyword.into());
295 self
296 }
297
298 pub fn search_text(&self) -> String {
300 let mut text = self.label.clone();
301 for field in self
302 .description
303 .iter()
304 .chain(self.metadata.iter().map(|(_, value)| value))
305 .chain(&self.keywords)
306 {
307 text.push('\n');
308 text.push_str(field);
309 }
310 text
311 }
312
313 pub fn action_for(&self, key: Key) -> Option<&Action> {
316 key.pick(self.actions.iter().map(|action| action.key.as_slice()))
317 .map(|at| &self.actions[at])
318 }
319
320 pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Item<U> {
322 Item {
323 value: f(self.value),
324 label: self.label,
325 description: self.description,
326 metadata: self.metadata,
327 preview: self.preview,
328 actions: self.actions,
329 keywords: self.keywords,
330 }
331 }
332}
333
334impl<T: fmt::Display> From<T> for Item<T> {
335 fn from(value: T) -> Item<T> {
337 let label = value.to_string();
338 Item::new(value, label)
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 #[test]
347 fn an_action_bound_to_the_key_itself_wins_over_an_earlier_match() {
348 let key = |name| Key::parse(name).unwrap();
349 let item = Item::new((), "row")
350 .action(Action::new("indent", "Indent", key("ctrl+i")))
351 .action(Action::new("next", "Next", key("tab")));
352 assert_eq!(item.action_for(key("tab")).unwrap().id, "next");
354 assert_eq!(item.action_for(key("ctrl+i")).unwrap().id, "indent");
355 let item = Item::new((), "row").action(Action::new("indent", "Indent", key("ctrl+i")));
356 assert_eq!(item.action_for(key("tab")).unwrap().id, "indent");
357 }
358
359 #[test]
360 fn builds_and_searches_items() {
361 let item = Item::new(std::path::PathBuf::from("src/main.rs"), "main.rs")
362 .description("the entry point")
363 .meta("size", "2 KB")
364 .keyword("src/main.rs")
365 .preview(Preview::Text("fn main() {}".into()))
366 .action(Action::new("edit", "Open in $EDITOR", Key::ctrl('e')));
367 assert_eq!(
368 item.search_text(),
369 "main.rs\nthe entry point\n2 KB\nsrc/main.rs"
370 );
371 assert_eq!(item.action_for(Key::ctrl('e')).unwrap().id, "edit");
372 assert!(item.action_for(Key::ctrl('d')).is_none());
373 let named = item.map(|path| path.display().to_string());
374 assert_eq!(named.value, "src/main.rs");
375 assert_eq!(Item::from(42).label, "42");
376 }
377
378 fn target(kind: TargetKind, value: &str) -> ActionTarget {
379 ActionTarget {
380 kind,
381 index: 0,
382 label: value.into(),
383 value: value.into(),
384 }
385 }
386
387 #[test]
388 fn view_actions_apply_by_target() {
389 let actions = Actions::new()
390 .action(Action::menu("copy", "Copy"))
391 .action_for(
392 TargetKind::File,
393 Action::new("edit", "Edit", Key::ctrl('e')),
394 )
395 .action_if(Action::menu("rust", "Rust only"), |target| {
396 target.value.ends_with(".rs")
397 });
398 let own = [Action::new("open", "Open", Key::ctrl('o'))];
399 let ids = |target: &ActionTarget| -> Vec<String> {
400 actions
401 .for_target(target, &own)
402 .into_iter()
403 .map(|action| action.id)
404 .collect()
405 };
406 assert_eq!(
407 ids(&target(TargetKind::File, "main.rs")),
408 ["open", "copy", "edit", "rust"]
409 );
410 assert_eq!(ids(&target(TargetKind::Row, "a\tb")), ["open", "copy"]);
411 let shadow = [Action::menu("copy", "Copy path")];
413 let found = actions.for_target(&target(TargetKind::Item, "x"), &shadow);
414 assert_eq!(found[0].label, "Copy path");
415 assert_eq!(found.len(), 1);
416 assert_eq!(TargetKind::parse("node"), Some(TargetKind::Node));
417 assert_eq!(TargetKind::Node.as_str(), "node");
418 }
419
420 #[test]
421 fn plugin_actions_come_from_the_registry() {
422 use rich_plugin_api::{CustomAction, Plugin, PluginError, PluginMetadata, PluginRegistrar};
423 struct Reveal;
424 impl CustomAction for Reveal {
425 fn label(&self) -> String {
426 "Reveal".into()
427 }
428 fn key(&self) -> Option<String> {
429 Some("ctrl+r".into())
430 }
431 fn applies(&self, kind: &str, value: &str) -> bool {
432 kind == "file" && !value.starts_with('.')
433 }
434 }
435 struct Files;
436 impl Plugin for Files {
437 fn metadata(&self) -> PluginMetadata {
438 PluginMetadata::new("files", "Files", "0.0.0")
439 }
440 fn register(&self, registrar: &mut dyn PluginRegistrar) -> Result<(), PluginError> {
441 registrar.action("reveal", Arc::new(Reveal));
442 Ok(())
443 }
444 }
445 let mut registry = rich_ext::registry::ExtensionRegistry::new();
446 registry.add_plugin(&Files).unwrap();
447 let actions = Actions::from_registry(®istry);
448 let found = actions.for_target(&target(TargetKind::File, "a.txt"), &[]);
449 assert_eq!(found, [Action::new("reveal", "Reveal", Key::ctrl('r'))]);
450 assert!(
451 actions
452 .for_target(&target(TargetKind::File, ".env"), &[])
453 .is_empty()
454 );
455 assert!(
456 actions
457 .for_target(&target(TargetKind::Item, "a.txt"), &[])
458 .is_empty()
459 );
460 }
461}