1use anyhow::Result;
2use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
3use client::UserId;
4use collections::HashMap;
5use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle};
6use fuzzy::StringMatchCandidate;
7use gpui::{
8 AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
9 Render, SharedString, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace,
10};
11use language::{
12 language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, Completion,
13 LanguageRegistry, LanguageServerId, ToOffset,
14};
15use lazy_static::lazy_static;
16use parking_lot::RwLock;
17use project::search::SearchQuery;
18use settings::Settings;
19use std::{sync::Arc, time::Duration};
20use theme::ThemeSettings;
21use ui::prelude::*;
22
23const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
24
25lazy_static! {
26 static ref MENTIONS_SEARCH: SearchQuery =
27 SearchQuery::regex("@[-_\\w]+", false, false, false, Vec::new(), Vec::new()).unwrap();
28}
29
30pub struct MessageEditor {
31 pub editor: View<Editor>,
32 channel_store: Model<ChannelStore>,
33 users: HashMap<String, UserId>,
34 mentions: Vec<UserId>,
35 mentions_task: Option<Task<()>>,
36 channel_id: Option<ChannelId>,
37}
38
39struct MessageEditorCompletionProvider(WeakView<MessageEditor>);
40
41impl CompletionProvider for MessageEditorCompletionProvider {
42 fn completions(
43 &self,
44 buffer: &Model<Buffer>,
45 buffer_position: language::Anchor,
46 cx: &mut ViewContext<Editor>,
47 ) -> Task<anyhow::Result<Vec<language::Completion>>> {
48 let Some(handle) = self.0.upgrade() else {
49 return Task::ready(Ok(Vec::new()));
50 };
51 handle.update(cx, |message_editor, cx| {
52 message_editor.completions(buffer, buffer_position, cx)
53 })
54 }
55
56 fn resolve_completions(
57 &self,
58 _completion_indices: Vec<usize>,
59 _completions: Arc<RwLock<Box<[language::Completion]>>>,
60 _cx: &mut ViewContext<Editor>,
61 ) -> Task<anyhow::Result<bool>> {
62 Task::ready(Ok(false))
63 }
64
65 fn apply_additional_edits_for_completion(
66 &self,
67 _buffer: Model<Buffer>,
68 _completion: Completion,
69 _push_to_history: bool,
70 _cx: &mut ViewContext<Editor>,
71 ) -> Task<Result<Option<language::Transaction>>> {
72 Task::ready(Ok(None))
73 }
74}
75
76impl MessageEditor {
77 pub fn new(
78 language_registry: Arc<LanguageRegistry>,
79 channel_store: Model<ChannelStore>,
80 editor: View<Editor>,
81 cx: &mut ViewContext<Self>,
82 ) -> Self {
83 let this = cx.view().downgrade();
84 editor.update(cx, |editor, cx| {
85 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
86 editor.set_completion_provider(Box::new(MessageEditorCompletionProvider(this)));
87 });
88
89 let buffer = editor
90 .read(cx)
91 .buffer()
92 .read(cx)
93 .as_singleton()
94 .expect("message editor must be singleton");
95
96 cx.subscribe(&buffer, Self::on_buffer_event).detach();
97
98 let markdown = language_registry.language_for_name("Markdown");
99 cx.spawn(|_, mut cx| async move {
100 let markdown = markdown.await?;
101 buffer.update(&mut cx, |buffer, cx| {
102 buffer.set_language(Some(markdown), cx)
103 })
104 })
105 .detach_and_log_err(cx);
106
107 Self {
108 editor,
109 channel_store,
110 users: HashMap::default(),
111 channel_id: None,
112 mentions: Vec::new(),
113 mentions_task: None,
114 }
115 }
116
117 pub fn set_channel(
118 &mut self,
119 channel_id: u64,
120 channel_name: Option<SharedString>,
121 cx: &mut ViewContext<Self>,
122 ) {
123 self.editor.update(cx, |editor, cx| {
124 if let Some(channel_name) = channel_name {
125 editor.set_placeholder_text(format!("Message #{}", channel_name), cx);
126 } else {
127 editor.set_placeholder_text(format!("Message Channel"), cx);
128 }
129 });
130 self.channel_id = Some(channel_id);
131 self.refresh_users(cx);
132 }
133
134 pub fn refresh_users(&mut self, cx: &mut ViewContext<Self>) {
135 if let Some(channel_id) = self.channel_id {
136 let members = self.channel_store.update(cx, |store, cx| {
137 store.get_channel_member_details(channel_id, cx)
138 });
139 cx.spawn(|this, mut cx| async move {
140 let members = members.await?;
141 this.update(&mut cx, |this, cx| this.set_members(members, cx))?;
142 anyhow::Ok(())
143 })
144 .detach_and_log_err(cx);
145 }
146 }
147
148 pub fn set_members(&mut self, members: Vec<ChannelMembership>, _: &mut ViewContext<Self>) {
149 self.users.clear();
150 self.users.extend(
151 members
152 .into_iter()
153 .map(|member| (member.user.github_login.clone(), member.user.id)),
154 );
155 }
156
157 pub fn take_message(&mut self, cx: &mut ViewContext<Self>) -> MessageParams {
158 self.editor.update(cx, |editor, cx| {
159 let highlights = editor.text_highlights::<Self>(cx);
160 let text = editor.text(cx);
161 let snapshot = editor.buffer().read(cx).snapshot(cx);
162 let mentions = if let Some((_, ranges)) = highlights {
163 ranges
164 .iter()
165 .map(|range| range.to_offset(&snapshot))
166 .zip(self.mentions.iter().copied())
167 .collect()
168 } else {
169 Vec::new()
170 };
171
172 editor.clear(cx);
173 self.mentions.clear();
174
175 MessageParams { text, mentions }
176 })
177 }
178
179 fn on_buffer_event(
180 &mut self,
181 buffer: Model<Buffer>,
182 event: &language::Event,
183 cx: &mut ViewContext<Self>,
184 ) {
185 if let language::Event::Reparsed | language::Event::Edited = event {
186 let buffer = buffer.read(cx).snapshot();
187 self.mentions_task = Some(cx.spawn(|this, cx| async move {
188 cx.background_executor()
189 .timer(MENTIONS_DEBOUNCE_INTERVAL)
190 .await;
191 Self::find_mentions(this, buffer, cx).await;
192 }));
193 }
194 }
195
196 fn completions(
197 &mut self,
198 buffer: &Model<Buffer>,
199 end_anchor: Anchor,
200 cx: &mut ViewContext<Self>,
201 ) -> Task<Result<Vec<Completion>>> {
202 let end_offset = end_anchor.to_offset(buffer.read(cx));
203
204 let Some(query) = buffer.update(cx, |buffer, _| {
205 let mut query = String::new();
206 for ch in buffer.reversed_chars_at(end_offset).take(100) {
207 if ch == '@' {
208 return Some(query.chars().rev().collect::<String>());
209 }
210 if ch.is_whitespace() || !ch.is_ascii() {
211 break;
212 }
213 query.push(ch);
214 }
215 return None;
216 }) else {
217 return Task::ready(Ok(vec![]));
218 };
219
220 let start_offset = end_offset - query.len();
221 let start_anchor = buffer.read(cx).anchor_before(start_offset);
222
223 let candidates = self
224 .users
225 .keys()
226 .map(|user| StringMatchCandidate {
227 id: 0,
228 string: user.clone(),
229 char_bag: user.chars().collect(),
230 })
231 .collect::<Vec<_>>();
232 cx.spawn(|_, cx| async move {
233 let matches = fuzzy::match_strings(
234 &candidates,
235 &query,
236 true,
237 10,
238 &Default::default(),
239 cx.background_executor().clone(),
240 )
241 .await;
242
243 Ok(matches
244 .into_iter()
245 .map(|mat| Completion {
246 old_range: start_anchor..end_anchor,
247 new_text: mat.string.clone(),
248 label: CodeLabel {
249 filter_range: 1..mat.string.len() + 1,
250 text: format!("@{}", mat.string),
251 runs: Vec::new(),
252 },
253 documentation: None,
254 server_id: LanguageServerId(0), // TODO: Make this optional or something?
255 lsp_completion: Default::default(), // TODO: Make this optional or something?
256 })
257 .collect())
258 })
259 }
260
261 async fn find_mentions(
262 this: WeakView<MessageEditor>,
263 buffer: BufferSnapshot,
264 mut cx: AsyncWindowContext,
265 ) {
266 let (buffer, ranges) = cx
267 .background_executor()
268 .spawn(async move {
269 let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
270 (buffer, ranges)
271 })
272 .await;
273
274 this.update(&mut cx, |this, cx| {
275 let mut anchor_ranges = Vec::new();
276 let mut mentioned_user_ids = Vec::new();
277 let mut text = String::new();
278
279 this.editor.update(cx, |editor, cx| {
280 let multi_buffer = editor.buffer().read(cx).snapshot(cx);
281 for range in ranges {
282 text.clear();
283 text.extend(buffer.text_for_range(range.clone()));
284 if let Some(username) = text.strip_prefix("@") {
285 if let Some(user_id) = this.users.get(username) {
286 let start = multi_buffer.anchor_after(range.start);
287 let end = multi_buffer.anchor_after(range.end);
288
289 mentioned_user_ids.push(*user_id);
290 anchor_ranges.push(start..end);
291 }
292 }
293 }
294
295 editor.clear_highlights::<Self>(cx);
296 editor.highlight_text::<Self>(
297 anchor_ranges,
298 HighlightStyle {
299 font_weight: Some(FontWeight::BOLD),
300 ..Default::default()
301 },
302 cx,
303 )
304 });
305
306 this.mentions = mentioned_user_ids;
307 this.mentions_task.take();
308 })
309 .ok();
310 }
311
312 pub(crate) fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
313 self.editor.read(cx).focus_handle(cx)
314 }
315}
316
317impl Render for MessageEditor {
318 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
319 let settings = ThemeSettings::get_global(cx);
320 let text_style = TextStyle {
321 color: if self.editor.read(cx).read_only(cx) {
322 cx.theme().colors().text_disabled
323 } else {
324 cx.theme().colors().text
325 },
326 font_family: settings.ui_font.family.clone(),
327 font_features: settings.ui_font.features,
328 font_size: rems(0.875).into(),
329 font_weight: FontWeight::NORMAL,
330 font_style: FontStyle::Normal,
331 line_height: relative(1.3).into(),
332 background_color: None,
333 underline: None,
334 white_space: WhiteSpace::Normal,
335 };
336
337 div()
338 .w_full()
339 .px_2()
340 .py_1()
341 .bg(cx.theme().colors().editor_background)
342 .rounded_md()
343 .child(EditorElement::new(
344 &self.editor,
345 EditorStyle {
346 local_player: cx.theme().players().local(),
347 text: text_style,
348 ..Default::default()
349 },
350 ))
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357 use client::{Client, User, UserStore};
358 use gpui::TestAppContext;
359 use language::{Language, LanguageConfig};
360 use rpc::proto;
361 use settings::SettingsStore;
362 use util::{http::FakeHttpClient, test::marked_text_ranges};
363
364 #[gpui::test]
365 async fn test_message_editor(cx: &mut TestAppContext) {
366 let language_registry = init_test(cx);
367
368 let (editor, cx) = cx.add_window_view(|cx| {
369 MessageEditor::new(
370 language_registry,
371 ChannelStore::global(cx),
372 cx.new_view(|cx| Editor::auto_height(4, cx)),
373 cx,
374 )
375 });
376 cx.executor().run_until_parked();
377
378 editor.update(cx, |editor, cx| {
379 editor.set_members(
380 vec![
381 ChannelMembership {
382 user: Arc::new(User {
383 github_login: "a-b".into(),
384 id: 101,
385 avatar_uri: "avatar_a-b".into(),
386 }),
387 kind: proto::channel_member::Kind::Member,
388 role: proto::ChannelRole::Member,
389 },
390 ChannelMembership {
391 user: Arc::new(User {
392 github_login: "C_D".into(),
393 id: 102,
394 avatar_uri: "avatar_C_D".into(),
395 }),
396 kind: proto::channel_member::Kind::Member,
397 role: proto::ChannelRole::Member,
398 },
399 ],
400 cx,
401 );
402
403 editor.editor.update(cx, |editor, cx| {
404 editor.set_text("Hello, @a-b! Have you met @C_D?", cx)
405 });
406 });
407
408 cx.executor().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
409
410 editor.update(cx, |editor, cx| {
411 let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
412 assert_eq!(
413 editor.take_message(cx),
414 MessageParams {
415 text,
416 mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
417 }
418 );
419 });
420 }
421
422 fn init_test(cx: &mut TestAppContext) -> Arc<LanguageRegistry> {
423 cx.update(|cx| {
424 let settings = SettingsStore::test(cx);
425 cx.set_global(settings);
426
427 let http = FakeHttpClient::with_404_response();
428 let client = Client::new(http.clone(), cx);
429 let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
430 theme::init(theme::LoadThemes::JustBase, cx);
431 language::init(cx);
432 editor::init(cx);
433 client::init(&client, cx);
434 channel::init(&client, user_store, cx);
435 });
436
437 let language_registry = Arc::new(LanguageRegistry::test());
438 language_registry.add(Arc::new(Language::new(
439 LanguageConfig {
440 name: "Markdown".into(),
441 ..Default::default()
442 },
443 Some(tree_sitter_markdown::language()),
444 )));
445 language_registry
446 }
447}