message_editor.rs

  1use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
  2use client::UserId;
  3use collections::HashMap;
  4use editor::{AnchorRangeExt, Editor};
  5use gpui::{
  6    elements::ChildView, AnyElement, AsyncAppContext, Element, Entity, ModelHandle, Task, View,
  7    ViewContext, ViewHandle, WeakViewHandle,
  8};
  9use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
 10use lazy_static::lazy_static;
 11use project::search::SearchQuery;
 12use std::{sync::Arc, time::Duration};
 13
 14const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
 15
 16lazy_static! {
 17    static ref MENTIONS_SEARCH: SearchQuery =
 18        SearchQuery::regex("@[-_\\w]+", false, false, false, Vec::new(), Vec::new()).unwrap();
 19}
 20
 21pub struct MessageEditor {
 22    pub editor: ViewHandle<Editor>,
 23    channel_store: ModelHandle<ChannelStore>,
 24    users: HashMap<String, UserId>,
 25    mentions: Vec<UserId>,
 26    mentions_task: Option<Task<()>>,
 27    channel_id: Option<ChannelId>,
 28}
 29
 30impl MessageEditor {
 31    pub fn new(
 32        language_registry: Arc<LanguageRegistry>,
 33        channel_store: ModelHandle<ChannelStore>,
 34        editor: ViewHandle<Editor>,
 35        cx: &mut ViewContext<Self>,
 36    ) -> Self {
 37        editor.update(cx, |editor, cx| {
 38            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
 39        });
 40
 41        let buffer = editor
 42            .read(cx)
 43            .buffer()
 44            .read(cx)
 45            .as_singleton()
 46            .expect("message editor must be singleton");
 47
 48        cx.subscribe(&buffer, Self::on_buffer_event).detach();
 49
 50        let markdown = language_registry.language_for_name("Markdown");
 51        cx.app_context()
 52            .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                anyhow::Ok(())
 58            })
 59            .detach_and_log_err(cx);
 60
 61        Self {
 62            editor,
 63            channel_store,
 64            users: HashMap::default(),
 65            channel_id: None,
 66            mentions: Vec::new(),
 67            mentions_task: None,
 68        }
 69    }
 70
 71    pub fn set_channel(
 72        &mut self,
 73        channel_id: u64,
 74        channel_name: Option<String>,
 75        cx: &mut ViewContext<Self>,
 76    ) {
 77        self.editor.update(cx, |editor, cx| {
 78            if let Some(channel_name) = channel_name {
 79                editor.set_placeholder_text(format!("Message #{}", channel_name), cx);
 80            } else {
 81                editor.set_placeholder_text(format!("Message Channel"), cx);
 82            }
 83        });
 84        self.channel_id = Some(channel_id);
 85        self.refresh_users(cx);
 86    }
 87
 88    pub fn refresh_users(&mut self, cx: &mut ViewContext<Self>) {
 89        if let Some(channel_id) = self.channel_id {
 90            let members = self.channel_store.update(cx, |store, cx| {
 91                store.get_channel_member_details(channel_id, cx)
 92            });
 93            cx.spawn(|this, mut cx| async move {
 94                let members = members.await?;
 95                this.update(&mut cx, |this, cx| this.set_members(members, cx))?;
 96                anyhow::Ok(())
 97            })
 98            .detach_and_log_err(cx);
 99        }
100    }
101
102    pub fn set_members(&mut self, members: Vec<ChannelMembership>, _: &mut ViewContext<Self>) {
103        self.users.clear();
104        self.users.extend(
105            members
106                .into_iter()
107                .map(|member| (member.user.github_login.clone(), member.user.id)),
108        );
109    }
110
111    pub fn take_message(&mut self, cx: &mut ViewContext<Self>) -> MessageParams {
112        self.editor.update(cx, |editor, cx| {
113            let highlights = editor.text_highlights::<Self>(cx);
114            let text = editor.text(cx);
115            let snapshot = editor.buffer().read(cx).snapshot(cx);
116            let mentions = if let Some((_, ranges)) = highlights {
117                ranges
118                    .iter()
119                    .map(|range| range.to_offset(&snapshot))
120                    .zip(self.mentions.iter().copied())
121                    .collect()
122            } else {
123                Vec::new()
124            };
125
126            editor.clear(cx);
127            self.mentions.clear();
128
129            MessageParams { text, mentions }
130        })
131    }
132
133    fn on_buffer_event(
134        &mut self,
135        buffer: ModelHandle<Buffer>,
136        event: &language::Event,
137        cx: &mut ViewContext<Self>,
138    ) {
139        if let language::Event::Reparsed | language::Event::Edited = event {
140            let buffer = buffer.read(cx).snapshot();
141            self.mentions_task = Some(cx.spawn(|this, cx| async move {
142                cx.background().timer(MENTIONS_DEBOUNCE_INTERVAL).await;
143                Self::find_mentions(this, buffer, cx).await;
144            }));
145        }
146    }
147
148    async fn find_mentions(
149        this: WeakViewHandle<MessageEditor>,
150        buffer: BufferSnapshot,
151        mut cx: AsyncAppContext,
152    ) {
153        let (buffer, ranges) = cx
154            .background()
155            .spawn(async move {
156                let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
157                (buffer, ranges)
158            })
159            .await;
160
161        this.update(&mut cx, |this, cx| {
162            let mut anchor_ranges = Vec::new();
163            let mut mentioned_user_ids = Vec::new();
164            let mut text = String::new();
165
166            this.editor.update(cx, |editor, cx| {
167                let multi_buffer = editor.buffer().read(cx).snapshot(cx);
168                for range in ranges {
169                    text.clear();
170                    text.extend(buffer.text_for_range(range.clone()));
171                    if let Some(username) = text.strip_prefix("@") {
172                        if let Some(user_id) = this.users.get(username) {
173                            let start = multi_buffer.anchor_after(range.start);
174                            let end = multi_buffer.anchor_after(range.end);
175
176                            mentioned_user_ids.push(*user_id);
177                            anchor_ranges.push(start..end);
178                        }
179                    }
180                }
181
182                editor.clear_highlights::<Self>(cx);
183                editor.highlight_text::<Self>(
184                    anchor_ranges,
185                    theme::current(cx).chat_panel.rich_text.mention_highlight,
186                    cx,
187                )
188            });
189
190            this.mentions = mentioned_user_ids;
191            this.mentions_task.take();
192        })
193        .ok();
194    }
195}
196
197impl Entity for MessageEditor {
198    type Event = ();
199}
200
201impl View for MessageEditor {
202    fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
203        ChildView::new(&self.editor, cx).into_any()
204    }
205
206    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
207        if cx.is_self_focused() {
208            cx.focus(&self.editor);
209        }
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use client::{Client, User, UserStore};
217    use gpui::{TestAppContext, WindowHandle};
218    use language::{Language, LanguageConfig};
219    use rpc::proto;
220    use settings::SettingsStore;
221    use util::{http::FakeHttpClient, test::marked_text_ranges};
222
223    #[gpui::test]
224    async fn test_message_editor(cx: &mut TestAppContext) {
225        let editor = init_test(cx);
226        let editor = editor.root(cx);
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: None,
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: None,
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.foreground().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) -> WindowHandle<MessageEditor> {
273        cx.foreground().forbid_parking();
274
275        cx.update(|cx| {
276            let http = FakeHttpClient::with_404_response();
277            let client = Client::new(http.clone(), cx);
278            let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
279            cx.set_global(SettingsStore::test(cx));
280            theme::init((), cx);
281            language::init(cx);
282            editor::init(cx);
283            client::init(&client, cx);
284            channel::init(&client, user_store, cx);
285        });
286
287        let language_registry = Arc::new(LanguageRegistry::test());
288        language_registry.add(Arc::new(Language::new(
289            LanguageConfig {
290                name: "Markdown".into(),
291                ..Default::default()
292            },
293            Some(tree_sitter_markdown::language()),
294        )));
295
296        let editor = cx.add_window(|cx| {
297            MessageEditor::new(
298                language_registry,
299                ChannelStore::global(cx),
300                cx.add_view(|cx| Editor::auto_height(4, None, cx)),
301                cx,
302            )
303        });
304        cx.foreground().run_until_parked();
305        editor
306    }
307}