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)]
207// mod tests {
208// use super::*;
209// use client::{Client, User, UserStore};
210// use gpui::{TestAppContext, WindowHandle};
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 editor = init_test(cx);
219// let editor = editor.root(cx);
220
221// editor.update(cx, |editor, cx| {
222// editor.set_members(
223// vec![
224// ChannelMembership {
225// user: Arc::new(User {
226// github_login: "a-b".into(),
227// id: 101,
228// avatar: None,
229// }),
230// kind: proto::channel_member::Kind::Member,
231// role: proto::ChannelRole::Member,
232// },
233// ChannelMembership {
234// user: Arc::new(User {
235// github_login: "C_D".into(),
236// id: 102,
237// avatar: None,
238// }),
239// kind: proto::channel_member::Kind::Member,
240// role: proto::ChannelRole::Member,
241// },
242// ],
243// cx,
244// );
245
246// editor.editor.update(cx, |editor, cx| {
247// editor.set_text("Hello, @a-b! Have you met @C_D?", cx)
248// });
249// });
250
251// cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
252
253// editor.update(cx, |editor, cx| {
254// let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
255// assert_eq!(
256// editor.take_message(cx),
257// MessageParams {
258// text,
259// mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
260// }
261// );
262// });
263// }
264
265// fn init_test(cx: &mut TestAppContext) -> WindowHandle<MessageEditor> {
266// cx.foreground().forbid_parking();
267
268// cx.update(|cx| {
269// let http = FakeHttpClient::with_404_response();
270// let client = Client::new(http.clone(), cx);
271// let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
272// cx.set_global(SettingsStore::test(cx));
273// theme::init((), cx);
274// language::init(cx);
275// editor::init(cx);
276// client::init(&client, cx);
277// channel::init(&client, user_store, cx);
278// });
279
280// let language_registry = Arc::new(LanguageRegistry::test());
281// language_registry.add(Arc::new(Language::new(
282// LanguageConfig {
283// name: "Markdown".into(),
284// ..Default::default()
285// },
286// Some(tree_sitter_markdown::language()),
287// )));
288
289// let editor = cx.add_window(|cx| {
290// MessageEditor::new(
291// language_registry,
292// ChannelStore::global(cx),
293// cx.add_view(|cx| Editor::auto_height(4, cx)),
294// cx,
295// )
296// });
297// cx.foreground().run_until_parked();
298// editor
299// }
300// }