Skip to main content

editor/
mention.rs

1//! The mention menu: entries an app offers, linked inline as chips.
2//!
3//! The app installs a [`MentionSource`] per trigger character with
4//! [`crate::AppExt::set_mention_source`]; with none installed no character
5//! opens a menu. The editor keeps focus while it is open and the query is the
6//! text typed after the trigger, the way the slash menu works. Picking a row
7//! replaces the trigger and query with a [`markdown::Mark::Mention`] chip
8//! linking its URL.
9
10use gpui::{App, Global, SharedString};
11use icons::Icon;
12use ui::menu::{self, Item};
13
14use markdown::Cursor;
15
16/// One row of the mention menu.
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct Mention {
19    pub icon: Option<Icon>,
20    pub label: SharedString,
21    pub description: Option<SharedString>,
22    /// What the chip links.
23    pub url: String,
24}
25
26/// The rows for what has been typed after the trigger, best first. Asked at
27/// paint.
28pub type MentionSource = fn(query: &str, cx: &App) -> Vec<Mention>;
29
30/// One source per trigger character.
31#[derive(Default)]
32pub(crate) struct Installed(pub Vec<(char, MentionSource)>);
33
34impl Global for Installed {}
35
36impl Installed {
37    pub(crate) fn source(cx: &App, trigger: &str) -> Option<(char, MentionSource)> {
38        let mut chars = trigger.chars();
39        let trigger = chars.next().filter(|_| chars.next().is_none())?;
40        cx.try_global::<Self>()?
41            .0
42            .iter()
43            .find(|(installed, _)| *installed == trigger)
44            .copied()
45    }
46}
47
48/// An open menu: where the trigger sits, what was typed after it, and the rows
49/// the source last gave for that.
50pub struct MentionMenu {
51    /// The trigger itself. Backspacing onto it closes the menu.
52    pub at: Cursor,
53    pub trigger: char,
54    source: MentionSource,
55    query: String,
56    /// The query `rows` answer, `None` until they are read.
57    asked: Option<String>,
58    rows: Vec<Mention>,
59    pub cursor: menu::Cursor,
60}
61
62impl MentionMenu {
63    pub fn open(at: Cursor, (trigger, source): (char, MentionSource)) -> Self {
64        Self {
65            at,
66            trigger,
67            source,
68            query: String::new(),
69            asked: None,
70            rows: Vec::new(),
71            cursor: menu::Cursor::default(),
72        }
73    }
74
75    pub fn set_query(&mut self, query: String) {
76        self.query = query;
77    }
78
79    /// Ask the source again when the query moved since it last answered.
80    pub fn refresh(&mut self, cx: &App) {
81        if self.asked.as_ref() == Some(&self.query) {
82            return;
83        }
84        self.rows = (self.source)(&self.query, cx);
85        self.asked = Some(self.query.clone());
86        self.cursor.clear();
87        self.cursor.step(&self.menu(), 1);
88    }
89
90    /// The rows as [`ui::menu::card`] paints them.
91    pub fn menu(&self) -> Vec<Item> {
92        self.rows
93            .iter()
94            .map(|row| {
95                let mut item = Item::action(row.label.clone());
96                if let Some(icon) = &row.icon {
97                    item = item.with_icon(icon.clone());
98                }
99                match &row.description {
100                    Some(description) => item.with_description(description.clone()),
101                    None => item,
102                }
103            })
104            .collect()
105    }
106
107    pub fn step(&mut self, delta: isize) {
108        let menu = self.menu();
109        self.cursor.step(&menu, delta);
110    }
111
112    /// The row at `ix`, or the live one.
113    pub fn row(&self, ix: Option<usize>) -> Option<&Mention> {
114        self.rows.get(ix.or(self.cursor.row())?)
115    }
116}