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