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