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