message_editor.rs

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