message_editor.rs

  1use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
  2use client::UserId;
  3use collections::HashMap;
  4use editor::{AnchorRangeExt, Editor};
  5use gpui::{
  6    AsyncWindowContext, FocusableView, IntoElement, Model, Render, SharedString, Task, View,
  7    ViewContext, WeakView,
  8};
  9use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
 10use lazy_static::lazy_static;
 11use project::search::SearchQuery;
 12use std::{sync::Arc, time::Duration};
 13use workspace::item::ItemHandle;
 14
 15const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
 16
 17lazy_static! {
 18    static ref MENTIONS_SEARCH: SearchQuery =
 19        SearchQuery::regex("@[-_\\w]+", false, false, false, Vec::new(), Vec::new()).unwrap();
 20}
 21
 22pub struct MessageEditor {
 23    pub editor: View<Editor>,
 24    channel_store: Model<ChannelStore>,
 25    users: HashMap<String, UserId>,
 26    mentions: Vec<UserId>,
 27    mentions_task: Option<Task<()>>,
 28    channel_id: Option<ChannelId>,
 29}
 30
 31impl MessageEditor {
 32    pub fn new(
 33        language_registry: Arc<LanguageRegistry>,
 34        channel_store: Model<ChannelStore>,
 35        editor: View<Editor>,
 36        cx: &mut ViewContext<Self>,
 37    ) -> Self {
 38        editor.update(cx, |editor, cx| {
 39            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
 40        });
 41
 42        let buffer = editor
 43            .read(cx)
 44            .buffer()
 45            .read(cx)
 46            .as_singleton()
 47            .expect("message editor must be singleton");
 48
 49        cx.subscribe(&buffer, Self::on_buffer_event).detach();
 50
 51        let markdown = language_registry.language_for_name("Markdown");
 52        cx.spawn(|_, mut cx| async move {
 53            let markdown = markdown.await?;
 54            buffer.update(&mut cx, |buffer, cx| {
 55                buffer.set_language(Some(markdown), cx)
 56            })
 57        })
 58        .detach_and_log_err(cx);
 59
 60        Self {
 61            editor,
 62            channel_store,
 63            users: HashMap::default(),
 64            channel_id: None,
 65            mentions: Vec::new(),
 66            mentions_task: None,
 67        }
 68    }
 69
 70    pub fn set_channel(
 71        &mut self,
 72        channel_id: u64,
 73        channel_name: Option<SharedString>,
 74        cx: &mut ViewContext<Self>,
 75    ) {
 76        self.editor.update(cx, |editor, cx| {
 77            if let Some(channel_name) = channel_name {
 78                editor.set_placeholder_text(format!("Message #{}", channel_name), cx);
 79            } else {
 80                editor.set_placeholder_text(format!("Message Channel"), cx);
 81            }
 82        });
 83        self.channel_id = Some(channel_id);
 84        self.refresh_users(cx);
 85    }
 86
 87    pub fn refresh_users(&mut self, cx: &mut ViewContext<Self>) {
 88        if let Some(channel_id) = self.channel_id {
 89            let members = self.channel_store.update(cx, |store, cx| {
 90                store.get_channel_member_details(channel_id, cx)
 91            });
 92            cx.spawn(|this, mut cx| async move {
 93                let members = members.await?;
 94                this.update(&mut cx, |this, cx| this.set_members(members, cx))?;
 95                anyhow::Ok(())
 96            })
 97            .detach_and_log_err(cx);
 98        }
 99    }
100
101    pub fn set_members(&mut self, members: Vec<ChannelMembership>, _: &mut ViewContext<Self>) {
102        self.users.clear();
103        self.users.extend(
104            members
105                .into_iter()
106                .map(|member| (member.user.github_login.clone(), member.user.id)),
107        );
108    }
109
110    pub fn take_message(&mut self, cx: &mut ViewContext<Self>) -> MessageParams {
111        self.editor.update(cx, |editor, cx| {
112            let highlights = editor.text_highlights::<Self>(cx);
113            let text = editor.text(cx);
114            let snapshot = editor.buffer().read(cx).snapshot(cx);
115            let mentions = if let Some((_, ranges)) = highlights {
116                ranges
117                    .iter()
118                    .map(|range| range.to_offset(&snapshot))
119                    .zip(self.mentions.iter().copied())
120                    .collect()
121            } else {
122                Vec::new()
123            };
124
125            editor.clear(cx);
126            self.mentions.clear();
127
128            MessageParams { text, mentions }
129        })
130    }
131
132    fn on_buffer_event(
133        &mut self,
134        buffer: Model<Buffer>,
135        event: &language::Event,
136        cx: &mut ViewContext<Self>,
137    ) {
138        if let language::Event::Reparsed | language::Event::Edited = event {
139            let buffer = buffer.read(cx).snapshot();
140            self.mentions_task = Some(cx.spawn(|this, cx| async move {
141                cx.background_executor()
142                    .timer(MENTIONS_DEBOUNCE_INTERVAL)
143                    .await;
144                Self::find_mentions(this, buffer, cx).await;
145            }));
146        }
147    }
148
149    async fn find_mentions(
150        this: WeakView<MessageEditor>,
151        buffer: BufferSnapshot,
152        mut cx: AsyncWindowContext,
153    ) {
154        let (buffer, ranges) = cx
155            .background_executor()
156            .spawn(async move {
157                let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
158                (buffer, ranges)
159            })
160            .await;
161
162        this.update(&mut cx, |this, cx| {
163            let mut anchor_ranges = Vec::new();
164            let mut mentioned_user_ids = Vec::new();
165            let mut text = String::new();
166
167            this.editor.update(cx, |editor, cx| {
168                let multi_buffer = editor.buffer().read(cx).snapshot(cx);
169                for range in ranges {
170                    text.clear();
171                    text.extend(buffer.text_for_range(range.clone()));
172                    if let Some(username) = text.strip_prefix("@") {
173                        if let Some(user_id) = this.users.get(username) {
174                            let start = multi_buffer.anchor_after(range.start);
175                            let end = multi_buffer.anchor_after(range.end);
176
177                            mentioned_user_ids.push(*user_id);
178                            anchor_ranges.push(start..end);
179                        }
180                    }
181                }
182
183                editor.clear_highlights::<Self>(cx);
184                editor.highlight_text::<Self>(anchor_ranges, gpui::red().into(), cx)
185            });
186
187            this.mentions = mentioned_user_ids;
188            this.mentions_task.take();
189        })
190        .ok();
191    }
192
193    pub(crate) fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
194        self.editor.read(cx).focus_handle(cx)
195    }
196}
197
198impl Render for MessageEditor {
199    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
200        self.editor.to_any()
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use client::{Client, User, UserStore};
208    use gpui::{Context as _, TestAppContext, VisualContext as _};
209    use language::{Language, LanguageConfig};
210    use rpc::proto;
211    use settings::SettingsStore;
212    use util::{http::FakeHttpClient, test::marked_text_ranges};
213
214    #[gpui::test]
215    async fn test_message_editor(cx: &mut TestAppContext) {
216        let language_registry = init_test(cx);
217
218        let (editor, cx) = cx.add_window_view(|cx| {
219            MessageEditor::new(
220                language_registry,
221                ChannelStore::global(cx),
222                cx.new_view(|cx| Editor::auto_height(4, cx)),
223                cx,
224            )
225        });
226        cx.executor().run_until_parked();
227
228        editor.update(cx, |editor, cx| {
229            editor.set_members(
230                vec![
231                    ChannelMembership {
232                        user: Arc::new(User {
233                            github_login: "a-b".into(),
234                            id: 101,
235                            avatar_uri: "avatar_a-b".into(),
236                        }),
237                        kind: proto::channel_member::Kind::Member,
238                        role: proto::ChannelRole::Member,
239                    },
240                    ChannelMembership {
241                        user: Arc::new(User {
242                            github_login: "C_D".into(),
243                            id: 102,
244                            avatar_uri: "avatar_C_D".into(),
245                        }),
246                        kind: proto::channel_member::Kind::Member,
247                        role: proto::ChannelRole::Member,
248                    },
249                ],
250                cx,
251            );
252
253            editor.editor.update(cx, |editor, cx| {
254                editor.set_text("Hello, @a-b! Have you met @C_D?", cx)
255            });
256        });
257
258        cx.executor().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
259
260        editor.update(cx, |editor, cx| {
261            let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
262            assert_eq!(
263                editor.take_message(cx),
264                MessageParams {
265                    text,
266                    mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
267                }
268            );
269        });
270    }
271
272    fn init_test(cx: &mut TestAppContext) -> Arc<LanguageRegistry> {
273        cx.update(|cx| {
274            let http = FakeHttpClient::with_404_response();
275            let client = Client::new(http.clone(), cx);
276            let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
277            let settings = SettingsStore::test(cx);
278            cx.set_global(settings);
279            theme::init(theme::LoadThemes::JustBase, cx);
280            language::init(cx);
281            editor::init(cx);
282            client::init(&client, cx);
283            channel::init(&client, user_store, cx);
284        });
285
286        let language_registry = Arc::new(LanguageRegistry::test());
287        language_registry.add(Arc::new(Language::new(
288            LanguageConfig {
289                name: "Markdown".into(),
290                ..Default::default()
291            },
292            Some(tree_sitter_markdown::language()),
293        )));
294        language_registry
295    }
296}