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}