1use channel::{Channel, ChannelStore};
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::{ops::Range, 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
36pub struct ChatMessage {
37 pub text: String,
38 pub mentions: Vec<(Range<usize>, UserId)>,
39}
40
41impl MessageEditor {
42 pub fn new(
43 language_registry: Arc<LanguageRegistry>,
44 channel_store: ModelHandle<ChannelStore>,
45 editor: ViewHandle<Editor>,
46 cx: &mut ViewContext<Self>,
47 ) -> Self {
48 editor.update(cx, |editor, cx| {
49 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
50 });
51
52 let buffer = editor
53 .read(cx)
54 .buffer()
55 .read(cx)
56 .as_singleton()
57 .expect("message editor must be singleton");
58
59 cx.subscribe(&buffer, Self::on_buffer_event).detach();
60 cx.subscribe(&editor, |_, _, event, cx| {
61 if let editor::Event::Focused = event {
62 eprintln!("focused");
63 cx.notify()
64 }
65 })
66 .detach();
67
68 let markdown = language_registry.language_for_name("Markdown");
69 cx.app_context()
70 .spawn(|mut cx| async move {
71 let markdown = markdown.await?;
72 buffer.update(&mut cx, |buffer, cx| {
73 buffer.set_language(Some(markdown), cx)
74 });
75 anyhow::Ok(())
76 })
77 .detach_and_log_err(cx);
78
79 Self {
80 editor,
81 channel_store,
82 users: HashMap::default(),
83 channel: None,
84 mentions: Vec::new(),
85 mentions_task: None,
86 }
87 }
88
89 pub fn set_channel(&mut self, channel: Arc<Channel>, cx: &mut ViewContext<Self>) {
90 self.editor.update(cx, |editor, cx| {
91 editor.set_placeholder_text(format!("Message #{}", channel.name), cx);
92 });
93 self.channel = Some(channel);
94 self.refresh_users(cx);
95 }
96
97 pub fn refresh_users(&mut self, cx: &mut ViewContext<Self>) {
98 if let Some(channel) = &self.channel {
99 let members = self.channel_store.update(cx, |store, cx| {
100 store.get_channel_member_details(channel.id, cx)
101 });
102 cx.spawn(|this, mut cx| async move {
103 let members = members.await?;
104 this.update(&mut cx, |this, _| {
105 this.users.clear();
106 this.users.extend(
107 members
108 .into_iter()
109 .map(|member| (member.user.github_login.clone(), member.user.id)),
110 );
111 })?;
112 anyhow::Ok(())
113 })
114 .detach_and_log_err(cx);
115 }
116 }
117
118 pub fn take_message(&mut self, cx: &mut ViewContext<Self>) -> ChatMessage {
119 self.editor.update(cx, |editor, cx| {
120 let highlights = editor.text_highlights::<Self>(cx);
121 let text = editor.text(cx);
122 let snapshot = editor.buffer().read(cx).snapshot(cx);
123 let mentions = if let Some((_, ranges)) = highlights {
124 ranges
125 .iter()
126 .map(|range| range.to_offset(&snapshot))
127 .zip(self.mentions.iter().copied())
128 .collect()
129 } else {
130 Vec::new()
131 };
132
133 editor.clear(cx);
134 self.mentions.clear();
135
136 ChatMessage { text, mentions }
137 })
138 }
139
140 fn on_buffer_event(
141 &mut self,
142 buffer: ModelHandle<Buffer>,
143 event: &language::Event,
144 cx: &mut ViewContext<Self>,
145 ) {
146 if let language::Event::Reparsed | language::Event::Edited = event {
147 let buffer = buffer.read(cx).snapshot();
148 self.mentions_task = Some(cx.spawn(|this, cx| async move {
149 cx.background().timer(MENTIONS_DEBOUNCE_INTERVAL).await;
150 Self::find_mentions(this, buffer, cx).await;
151 }));
152 }
153 }
154
155 async fn find_mentions(
156 this: WeakViewHandle<MessageEditor>,
157 buffer: BufferSnapshot,
158 mut cx: AsyncAppContext,
159 ) {
160 let (buffer, ranges) = cx
161 .background()
162 .spawn(async move {
163 let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
164 (buffer, ranges)
165 })
166 .await;
167
168 this.update(&mut cx, |this, cx| {
169 let mut anchor_ranges = Vec::new();
170 let mut mentioned_user_ids = Vec::new();
171 let mut text = String::new();
172
173 this.editor.update(cx, |editor, cx| {
174 let multi_buffer = editor.buffer().read(cx).snapshot(cx);
175 for range in ranges {
176 text.clear();
177 text.extend(buffer.text_for_range(range.clone()));
178 if let Some(username) = text.strip_prefix("@") {
179 if let Some(user_id) = this.users.get(username) {
180 let start = multi_buffer.anchor_after(range.start);
181 let end = multi_buffer.anchor_after(range.end);
182
183 mentioned_user_ids.push(*user_id);
184 anchor_ranges.push(start..end);
185 }
186 }
187 }
188
189 editor.clear_highlights::<Self>(cx);
190 editor.highlight_text::<Self>(
191 anchor_ranges,
192 theme::current(cx).chat_panel.mention_highlight,
193 cx,
194 )
195 });
196
197 this.mentions = mentioned_user_ids;
198 this.mentions_task.take();
199 })
200 .ok();
201 }
202}
203
204impl Entity for MessageEditor {
205 type Event = ();
206}
207
208impl View for MessageEditor {
209 fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
210 ChildView::new(&self.editor, cx).into_any()
211 }
212
213 fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
214 if cx.is_self_focused() {
215 cx.focus(&self.editor);
216 }
217 }
218}