Skip to main content

editor/
mention.rs

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