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