1use crate::{
2 acp::completion_provider::ContextPickerCompletionProvider,
3 context_picker::fetch_context_picker::fetch_url_content,
4};
5use acp_thread::{MentionUri, selection_name};
6use agent::{TextThreadStore, ThreadId, ThreadStore};
7use agent_client_protocol as acp;
8use anyhow::{Context as _, Result, anyhow};
9use collections::{HashMap, HashSet};
10use editor::{
11 Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
12 EditorMode, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer, ToOffset,
13 actions::Paste,
14 display_map::{Crease, CreaseId, FoldId},
15};
16use futures::{
17 FutureExt as _, TryFutureExt as _,
18 future::{Shared, try_join_all},
19};
20use gpui::{
21 AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, Image,
22 ImageFormat, Img, Task, TextStyle, WeakEntity,
23};
24use language::{Buffer, Language};
25use language_model::LanguageModelImage;
26use project::{CompletionIntent, Project};
27use rope::Point;
28use settings::Settings;
29use std::{
30 ffi::OsStr,
31 fmt::Write,
32 ops::Range,
33 path::{Path, PathBuf},
34 rc::Rc,
35 sync::Arc,
36};
37use text::OffsetRangeExt;
38use theme::ThemeSettings;
39use ui::{
40 ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Icon, IconName,
41 IconSize, InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ParentElement,
42 Render, SelectableButton, SharedString, Styled, TextSize, TintColor, Toggleable, Window, div,
43 h_flex,
44};
45use url::Url;
46use util::ResultExt;
47use workspace::{Workspace, notifications::NotifyResultExt as _};
48use zed_actions::agent::Chat;
49
50pub struct MessageEditor {
51 mention_set: MentionSet,
52 editor: Entity<Editor>,
53 project: Entity<Project>,
54 workspace: WeakEntity<Workspace>,
55 thread_store: Entity<ThreadStore>,
56 text_thread_store: Entity<TextThreadStore>,
57}
58
59#[derive(Clone, Copy)]
60pub enum MessageEditorEvent {
61 Send,
62 Cancel,
63 Focus,
64}
65
66impl EventEmitter<MessageEditorEvent> for MessageEditor {}
67
68impl MessageEditor {
69 pub fn new(
70 workspace: WeakEntity<Workspace>,
71 project: Entity<Project>,
72 thread_store: Entity<ThreadStore>,
73 text_thread_store: Entity<TextThreadStore>,
74 placeholder: impl Into<Arc<str>>,
75 mode: EditorMode,
76 window: &mut Window,
77 cx: &mut Context<Self>,
78 ) -> Self {
79 let language = Language::new(
80 language::LanguageConfig {
81 completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
82 ..Default::default()
83 },
84 None,
85 );
86 let completion_provider = ContextPickerCompletionProvider::new(
87 workspace.clone(),
88 thread_store.downgrade(),
89 text_thread_store.downgrade(),
90 cx.weak_entity(),
91 );
92 let mention_set = MentionSet::default();
93 let editor = cx.new(|cx| {
94 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
95 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
96
97 let mut editor = Editor::new(mode, buffer, None, window, cx);
98 editor.set_placeholder_text(placeholder, cx);
99 editor.set_show_indent_guides(false, cx);
100 editor.set_soft_wrap();
101 editor.set_use_modal_editing(true);
102 editor.set_completion_provider(Some(Rc::new(completion_provider)));
103 editor.set_context_menu_options(ContextMenuOptions {
104 min_entries_visible: 12,
105 max_entries_visible: 12,
106 placement: Some(ContextMenuPlacement::Above),
107 });
108 editor
109 });
110
111 cx.on_focus(&editor.focus_handle(cx), window, |_, _, cx| {
112 cx.emit(MessageEditorEvent::Focus)
113 })
114 .detach();
115
116 Self {
117 editor,
118 project,
119 mention_set,
120 thread_store,
121 text_thread_store,
122 workspace,
123 }
124 }
125
126 #[cfg(test)]
127 pub(crate) fn editor(&self) -> &Entity<Editor> {
128 &self.editor
129 }
130
131 #[cfg(test)]
132 pub(crate) fn mention_set(&mut self) -> &mut MentionSet {
133 &mut self.mention_set
134 }
135
136 pub fn is_empty(&self, cx: &App) -> bool {
137 self.editor.read(cx).is_empty(cx)
138 }
139
140 pub fn mentioned_path_and_threads(&self) -> (HashSet<PathBuf>, HashSet<ThreadId>) {
141 let mut excluded_paths = HashSet::default();
142 let mut excluded_threads = HashSet::default();
143
144 for uri in self.mention_set.uri_by_crease_id.values() {
145 match uri {
146 MentionUri::File { abs_path, .. } => {
147 excluded_paths.insert(abs_path.clone());
148 }
149 MentionUri::Thread { id, .. } => {
150 excluded_threads.insert(id.clone());
151 }
152 _ => {}
153 }
154 }
155
156 (excluded_paths, excluded_threads)
157 }
158
159 pub fn confirm_completion(
160 &mut self,
161 crease_text: SharedString,
162 start: text::Anchor,
163 content_len: usize,
164 mention_uri: MentionUri,
165 window: &mut Window,
166 cx: &mut Context<Self>,
167 ) {
168 let snapshot = self
169 .editor
170 .update(cx, |editor, cx| editor.snapshot(window, cx));
171 let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else {
172 return;
173 };
174 let Some(anchor) = snapshot
175 .buffer_snapshot
176 .anchor_in_excerpt(*excerpt_id, start)
177 else {
178 return;
179 };
180
181 let Some(crease_id) = crate::context_picker::insert_crease_for_mention(
182 *excerpt_id,
183 start,
184 content_len,
185 crease_text.clone(),
186 mention_uri.icon_path(cx),
187 self.editor.clone(),
188 window,
189 cx,
190 ) else {
191 return;
192 };
193
194 match mention_uri {
195 MentionUri::Fetch { url } => {
196 self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx);
197 }
198 MentionUri::File {
199 abs_path,
200 is_directory,
201 } => {
202 self.confirm_mention_for_file(
203 crease_id,
204 anchor,
205 abs_path,
206 is_directory,
207 window,
208 cx,
209 );
210 }
211 MentionUri::Thread { id, name } => {
212 self.confirm_mention_for_thread(crease_id, anchor, id, name, window, cx);
213 }
214 MentionUri::TextThread { path, name } => {
215 self.confirm_mention_for_text_thread(crease_id, anchor, path, name, window, cx);
216 }
217 MentionUri::Symbol { .. } | MentionUri::Rule { .. } | MentionUri::Selection { .. } => {
218 self.mention_set.insert_uri(crease_id, mention_uri.clone());
219 }
220 }
221 }
222
223 fn confirm_mention_for_file(
224 &mut self,
225 crease_id: CreaseId,
226 anchor: Anchor,
227 abs_path: PathBuf,
228 is_directory: bool,
229 window: &mut Window,
230 cx: &mut Context<Self>,
231 ) {
232 let extension = abs_path
233 .extension()
234 .and_then(OsStr::to_str)
235 .unwrap_or_default();
236
237 if Img::extensions().contains(&extension) && !extension.contains("svg") {
238 let project = self.project.clone();
239 let Some(project_path) = project
240 .read(cx)
241 .project_path_for_absolute_path(&abs_path, cx)
242 else {
243 return;
244 };
245 let image = cx.spawn(async move |_, cx| {
246 let image = project
247 .update(cx, |project, cx| project.open_image(project_path, cx))?
248 .await?;
249 image.read_with(cx, |image, _cx| image.image.clone())
250 });
251 self.confirm_mention_for_image(crease_id, anchor, Some(abs_path), image, window, cx);
252 } else {
253 self.mention_set.insert_uri(
254 crease_id,
255 MentionUri::File {
256 abs_path,
257 is_directory,
258 },
259 );
260 }
261 }
262
263 fn confirm_mention_for_fetch(
264 &mut self,
265 crease_id: CreaseId,
266 anchor: Anchor,
267 url: url::Url,
268 window: &mut Window,
269 cx: &mut Context<Self>,
270 ) {
271 let Some(http_client) = self
272 .workspace
273 .update(cx, |workspace, _cx| workspace.client().http_client())
274 .ok()
275 else {
276 return;
277 };
278
279 let url_string = url.to_string();
280 let fetch = cx
281 .background_executor()
282 .spawn(async move {
283 fetch_url_content(http_client, url_string)
284 .map_err(|e| e.to_string())
285 .await
286 })
287 .shared();
288 self.mention_set
289 .add_fetch_result(url.clone(), fetch.clone());
290
291 cx.spawn_in(window, async move |this, cx| {
292 let fetch = fetch.await.notify_async_err(cx);
293 this.update(cx, |this, cx| {
294 let mention_uri = MentionUri::Fetch { url };
295 if fetch.is_some() {
296 this.mention_set.insert_uri(crease_id, mention_uri.clone());
297 } else {
298 // Remove crease if we failed to fetch
299 this.editor.update(cx, |editor, cx| {
300 editor.display_map.update(cx, |display_map, cx| {
301 display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
302 });
303 editor.remove_creases([crease_id], cx);
304 });
305 }
306 })
307 .ok();
308 })
309 .detach();
310 }
311
312 pub fn confirm_mention_for_selection(
313 &mut self,
314 source_range: Range<text::Anchor>,
315 selections: Vec<(Entity<Buffer>, Range<text::Anchor>, Range<usize>)>,
316 window: &mut Window,
317 cx: &mut Context<Self>,
318 ) {
319 let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
320 let Some((&excerpt_id, _, _)) = snapshot.as_singleton() else {
321 return;
322 };
323 let Some(start) = snapshot.anchor_in_excerpt(excerpt_id, source_range.start) else {
324 return;
325 };
326
327 let offset = start.to_offset(&snapshot);
328
329 for (buffer, selection_range, range_to_fold) in selections {
330 let range = snapshot.anchor_after(offset + range_to_fold.start)
331 ..snapshot.anchor_after(offset + range_to_fold.end);
332
333 let path = buffer
334 .read(cx)
335 .file()
336 .map_or(PathBuf::from("untitled"), |file| file.path().to_path_buf());
337 let snapshot = buffer.read(cx).snapshot();
338
339 let point_range = selection_range.to_point(&snapshot);
340 let line_range = point_range.start.row..point_range.end.row;
341
342 let uri = MentionUri::Selection {
343 path: path.clone(),
344 line_range: line_range.clone(),
345 };
346 let crease = crate::context_picker::crease_for_mention(
347 selection_name(&path, &line_range).into(),
348 uri.icon_path(cx),
349 range,
350 self.editor.downgrade(),
351 );
352
353 let crease_id = self.editor.update(cx, |editor, cx| {
354 let crease_ids = editor.insert_creases(vec![crease.clone()], cx);
355 editor.fold_creases(vec![crease], false, window, cx);
356 crease_ids.first().copied().unwrap()
357 });
358
359 self.mention_set
360 .insert_uri(crease_id, MentionUri::Selection { path, line_range });
361 }
362 }
363
364 pub fn contents(
365 &self,
366 window: &mut Window,
367 cx: &mut Context<Self>,
368 ) -> Task<Result<Vec<acp::ContentBlock>>> {
369 let contents =
370 self.mention_set
371 .contents(self.project.clone(), self.thread_store.clone(), window, cx);
372 let editor = self.editor.clone();
373
374 cx.spawn(async move |_, cx| {
375 let contents = contents.await?;
376
377 editor.update(cx, |editor, cx| {
378 let mut ix = 0;
379 let mut chunks: Vec<acp::ContentBlock> = Vec::new();
380 let text = editor.text(cx);
381 editor.display_map.update(cx, |map, cx| {
382 let snapshot = map.snapshot(cx);
383 for (crease_id, crease) in snapshot.crease_snapshot.creases() {
384 // Skip creases that have been edited out of the message buffer.
385 if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
386 continue;
387 }
388
389 let Some(mention) = contents.get(&crease_id) else {
390 continue;
391 };
392
393 let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
394 if crease_range.start > ix {
395 chunks.push(text[ix..crease_range.start].into());
396 }
397 let chunk = match mention {
398 Mention::Text { uri, content } => {
399 acp::ContentBlock::Resource(acp::EmbeddedResource {
400 annotations: None,
401 resource: acp::EmbeddedResourceResource::TextResourceContents(
402 acp::TextResourceContents {
403 mime_type: None,
404 text: content.clone(),
405 uri: uri.to_uri().to_string(),
406 },
407 ),
408 })
409 }
410 Mention::Image(mention_image) => {
411 acp::ContentBlock::Image(acp::ImageContent {
412 annotations: None,
413 data: mention_image.data.to_string(),
414 mime_type: mention_image.format.mime_type().into(),
415 uri: mention_image
416 .abs_path
417 .as_ref()
418 .map(|path| format!("file://{}", path.display())),
419 })
420 }
421 };
422 chunks.push(chunk);
423 ix = crease_range.end;
424 }
425
426 if ix < text.len() {
427 let last_chunk = text[ix..].trim_end();
428 if !last_chunk.is_empty() {
429 chunks.push(last_chunk.into());
430 }
431 }
432 });
433
434 chunks
435 })
436 })
437 }
438
439 pub fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
440 self.editor.update(cx, |editor, cx| {
441 editor.clear(window, cx);
442 editor.remove_creases(self.mention_set.drain(), cx)
443 });
444 }
445
446 fn send(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
447 cx.emit(MessageEditorEvent::Send)
448 }
449
450 fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
451 cx.emit(MessageEditorEvent::Cancel)
452 }
453
454 fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
455 let images = cx
456 .read_from_clipboard()
457 .map(|item| {
458 item.into_entries()
459 .filter_map(|entry| {
460 if let ClipboardEntry::Image(image) = entry {
461 Some(image)
462 } else {
463 None
464 }
465 })
466 .collect::<Vec<_>>()
467 })
468 .unwrap_or_default();
469
470 if images.is_empty() {
471 return;
472 }
473 cx.stop_propagation();
474
475 let replacement_text = "image";
476 for image in images {
477 let (excerpt_id, text_anchor, multibuffer_anchor) =
478 self.editor.update(cx, |message_editor, cx| {
479 let snapshot = message_editor.snapshot(window, cx);
480 let (excerpt_id, _, buffer_snapshot) =
481 snapshot.buffer_snapshot.as_singleton().unwrap();
482
483 let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
484 let multibuffer_anchor = snapshot
485 .buffer_snapshot
486 .anchor_in_excerpt(*excerpt_id, text_anchor);
487 message_editor.edit(
488 [(
489 multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
490 format!("{replacement_text} "),
491 )],
492 cx,
493 );
494 (*excerpt_id, text_anchor, multibuffer_anchor)
495 });
496
497 let content_len = replacement_text.len();
498 let Some(anchor) = multibuffer_anchor else {
499 return;
500 };
501 let Some(crease_id) = insert_crease_for_image(
502 excerpt_id,
503 text_anchor,
504 content_len,
505 None.clone(),
506 self.editor.clone(),
507 window,
508 cx,
509 ) else {
510 return;
511 };
512 self.confirm_mention_for_image(
513 crease_id,
514 anchor,
515 None,
516 Task::ready(Ok(Arc::new(image))),
517 window,
518 cx,
519 );
520 }
521 }
522
523 pub fn insert_dragged_files(
524 &self,
525 paths: Vec<project::ProjectPath>,
526 window: &mut Window,
527 cx: &mut Context<Self>,
528 ) {
529 let buffer = self.editor.read(cx).buffer().clone();
530 let Some(buffer) = buffer.read(cx).as_singleton() else {
531 return;
532 };
533 for path in paths {
534 let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
535 continue;
536 };
537 let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else {
538 continue;
539 };
540
541 let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
542 let path_prefix = abs_path
543 .file_name()
544 .unwrap_or(path.path.as_os_str())
545 .display()
546 .to_string();
547 let Some(completion) = ContextPickerCompletionProvider::completion_for_path(
548 path,
549 &path_prefix,
550 false,
551 entry.is_dir(),
552 anchor..anchor,
553 cx.weak_entity(),
554 self.project.clone(),
555 cx,
556 ) else {
557 continue;
558 };
559
560 self.editor.update(cx, |message_editor, cx| {
561 message_editor.edit(
562 [(
563 multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
564 completion.new_text,
565 )],
566 cx,
567 );
568 });
569 if let Some(confirm) = completion.confirm.clone() {
570 confirm(CompletionIntent::Complete, window, cx);
571 }
572 }
573 }
574
575 pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
576 self.editor.update(cx, |message_editor, cx| {
577 message_editor.set_read_only(read_only);
578 cx.notify()
579 })
580 }
581
582 fn confirm_mention_for_image(
583 &mut self,
584 crease_id: CreaseId,
585 anchor: Anchor,
586 abs_path: Option<PathBuf>,
587 image: Task<Result<Arc<Image>>>,
588 window: &mut Window,
589 cx: &mut Context<Self>,
590 ) {
591 let editor = self.editor.clone();
592 let task = cx
593 .spawn_in(window, {
594 let abs_path = abs_path.clone();
595 async move |_, cx| {
596 let image = image.await.map_err(|e| e.to_string())?;
597 let format = image.format;
598 let image = cx
599 .update(|_, cx| LanguageModelImage::from_image(image, cx))
600 .map_err(|e| e.to_string())?
601 .await;
602 if let Some(image) = image {
603 Ok(MentionImage {
604 abs_path,
605 data: image.source,
606 format,
607 })
608 } else {
609 Err("Failed to convert image".into())
610 }
611 }
612 })
613 .shared();
614
615 self.mention_set.insert_image(crease_id, task.clone());
616
617 cx.spawn_in(window, async move |this, cx| {
618 if task.await.notify_async_err(cx).is_some() {
619 if let Some(abs_path) = abs_path.clone() {
620 this.update(cx, |this, _cx| {
621 this.mention_set.insert_uri(
622 crease_id,
623 MentionUri::File {
624 abs_path,
625 is_directory: false,
626 },
627 );
628 })
629 .ok();
630 }
631 } else {
632 editor
633 .update(cx, |editor, cx| {
634 editor.display_map.update(cx, |display_map, cx| {
635 display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
636 });
637 editor.remove_creases([crease_id], cx);
638 })
639 .ok();
640 }
641 })
642 .detach();
643 }
644
645 fn confirm_mention_for_thread(
646 &mut self,
647 crease_id: CreaseId,
648 anchor: Anchor,
649 id: ThreadId,
650 name: String,
651 window: &mut Window,
652 cx: &mut Context<Self>,
653 ) {
654 let uri = MentionUri::Thread {
655 id: id.clone(),
656 name,
657 };
658 let open_task = self.thread_store.update(cx, |thread_store, cx| {
659 thread_store.open_thread(&id, window, cx)
660 });
661 let task = cx
662 .spawn(async move |_, cx| {
663 let thread = open_task.await.map_err(|e| e.to_string())?;
664 let content = thread
665 .read_with(cx, |thread, _cx| thread.latest_detailed_summary_or_text())
666 .map_err(|e| e.to_string())?;
667 Ok(content)
668 })
669 .shared();
670
671 self.mention_set.insert_thread(id, task.clone());
672
673 let editor = self.editor.clone();
674 cx.spawn_in(window, async move |this, cx| {
675 if task.await.notify_async_err(cx).is_some() {
676 this.update(cx, |this, _| {
677 this.mention_set.insert_uri(crease_id, uri);
678 })
679 .ok();
680 } else {
681 editor
682 .update(cx, |editor, cx| {
683 editor.display_map.update(cx, |display_map, cx| {
684 display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
685 });
686 editor.remove_creases([crease_id], cx);
687 })
688 .ok();
689 }
690 })
691 .detach();
692 }
693
694 fn confirm_mention_for_text_thread(
695 &mut self,
696 crease_id: CreaseId,
697 anchor: Anchor,
698 path: PathBuf,
699 name: String,
700 window: &mut Window,
701 cx: &mut Context<Self>,
702 ) {
703 let uri = MentionUri::TextThread {
704 path: path.clone(),
705 name,
706 };
707 let context = self.text_thread_store.update(cx, |text_thread_store, cx| {
708 text_thread_store.open_local_context(path.as_path().into(), cx)
709 });
710 let task = cx
711 .spawn(async move |_, cx| {
712 let context = context.await.map_err(|e| e.to_string())?;
713 let xml = context
714 .update(cx, |context, cx| context.to_xml(cx))
715 .map_err(|e| e.to_string())?;
716 Ok(xml)
717 })
718 .shared();
719
720 self.mention_set.insert_text_thread(path, task.clone());
721
722 let editor = self.editor.clone();
723 cx.spawn_in(window, async move |this, cx| {
724 if task.await.notify_async_err(cx).is_some() {
725 this.update(cx, |this, _| {
726 this.mention_set.insert_uri(crease_id, uri);
727 })
728 .ok();
729 } else {
730 editor
731 .update(cx, |editor, cx| {
732 editor.display_map.update(cx, |display_map, cx| {
733 display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
734 });
735 editor.remove_creases([crease_id], cx);
736 })
737 .ok();
738 }
739 })
740 .detach();
741 }
742
743 pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
744 self.editor.update(cx, |editor, cx| {
745 editor.set_mode(mode);
746 cx.notify()
747 });
748 }
749
750 pub fn set_message(
751 &mut self,
752 message: Vec<acp::ContentBlock>,
753 window: &mut Window,
754 cx: &mut Context<Self>,
755 ) {
756 self.clear(window, cx);
757
758 let mut text = String::new();
759 let mut mentions = Vec::new();
760 let mut images = Vec::new();
761
762 for chunk in message {
763 match chunk {
764 acp::ContentBlock::Text(text_content) => {
765 text.push_str(&text_content.text);
766 }
767 acp::ContentBlock::Resource(acp::EmbeddedResource {
768 resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
769 ..
770 }) => {
771 if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() {
772 let start = text.len();
773 write!(&mut text, "{}", mention_uri.as_link()).ok();
774 let end = text.len();
775 mentions.push((start..end, mention_uri, resource.text));
776 }
777 }
778 acp::ContentBlock::Image(content) => {
779 let start = text.len();
780 text.push_str("image");
781 let end = text.len();
782 images.push((start..end, content));
783 }
784 acp::ContentBlock::Audio(_)
785 | acp::ContentBlock::Resource(_)
786 | acp::ContentBlock::ResourceLink(_) => {}
787 }
788 }
789
790 let snapshot = self.editor.update(cx, |editor, cx| {
791 editor.set_text(text, window, cx);
792 editor.buffer().read(cx).snapshot(cx)
793 });
794
795 for (range, mention_uri, text) in mentions {
796 let anchor = snapshot.anchor_before(range.start);
797 let crease_id = crate::context_picker::insert_crease_for_mention(
798 anchor.excerpt_id,
799 anchor.text_anchor,
800 range.end - range.start,
801 mention_uri.name().into(),
802 mention_uri.icon_path(cx),
803 self.editor.clone(),
804 window,
805 cx,
806 );
807
808 if let Some(crease_id) = crease_id {
809 self.mention_set.insert_uri(crease_id, mention_uri.clone());
810 }
811
812 match mention_uri {
813 MentionUri::Thread { id, .. } => {
814 self.mention_set
815 .insert_thread(id, Task::ready(Ok(text.into())).shared());
816 }
817 MentionUri::TextThread { path, .. } => {
818 self.mention_set
819 .insert_text_thread(path, Task::ready(Ok(text)).shared());
820 }
821 MentionUri::Fetch { url } => {
822 self.mention_set
823 .add_fetch_result(url, Task::ready(Ok(text)).shared());
824 }
825 MentionUri::File { .. }
826 | MentionUri::Symbol { .. }
827 | MentionUri::Rule { .. }
828 | MentionUri::Selection { .. } => {}
829 }
830 }
831 for (range, content) in images {
832 let Some(format) = ImageFormat::from_mime_type(&content.mime_type) else {
833 continue;
834 };
835 let anchor = snapshot.anchor_before(range.start);
836 let abs_path = content
837 .uri
838 .as_ref()
839 .and_then(|uri| uri.strip_prefix("file://").map(|s| Path::new(s).into()));
840
841 let name = content
842 .uri
843 .as_ref()
844 .and_then(|uri| {
845 uri.strip_prefix("file://")
846 .and_then(|path| Path::new(path).file_name())
847 })
848 .map(|name| name.to_string_lossy().to_string())
849 .unwrap_or("Image".to_owned());
850 let crease_id = crate::context_picker::insert_crease_for_mention(
851 anchor.excerpt_id,
852 anchor.text_anchor,
853 range.end - range.start,
854 name.into(),
855 IconName::Image.path().into(),
856 self.editor.clone(),
857 window,
858 cx,
859 );
860 let data: SharedString = content.data.to_string().into();
861
862 if let Some(crease_id) = crease_id {
863 self.mention_set.insert_image(
864 crease_id,
865 Task::ready(Ok(MentionImage {
866 abs_path,
867 data,
868 format,
869 }))
870 .shared(),
871 );
872 }
873 }
874 cx.notify();
875 }
876
877 #[cfg(test)]
878 pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
879 self.editor.update(cx, |editor, cx| {
880 editor.set_text(text, window, cx);
881 });
882 }
883
884 #[cfg(test)]
885 pub fn text(&self, cx: &App) -> String {
886 self.editor.read(cx).text(cx)
887 }
888}
889
890impl Focusable for MessageEditor {
891 fn focus_handle(&self, cx: &App) -> FocusHandle {
892 self.editor.focus_handle(cx)
893 }
894}
895
896impl Render for MessageEditor {
897 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
898 div()
899 .key_context("MessageEditor")
900 .on_action(cx.listener(Self::send))
901 .on_action(cx.listener(Self::cancel))
902 .capture_action(cx.listener(Self::paste))
903 .flex_1()
904 .child({
905 let settings = ThemeSettings::get_global(cx);
906 let font_size = TextSize::Small
907 .rems(cx)
908 .to_pixels(settings.agent_font_size(cx));
909 let line_height = settings.buffer_line_height.value() * font_size;
910
911 let text_style = TextStyle {
912 color: cx.theme().colors().text,
913 font_family: settings.buffer_font.family.clone(),
914 font_fallbacks: settings.buffer_font.fallbacks.clone(),
915 font_features: settings.buffer_font.features.clone(),
916 font_size: font_size.into(),
917 line_height: line_height.into(),
918 ..Default::default()
919 };
920
921 EditorElement::new(
922 &self.editor,
923 EditorStyle {
924 background: cx.theme().colors().editor_background,
925 local_player: cx.theme().players().local(),
926 text: text_style,
927 syntax: cx.theme().syntax().clone(),
928 ..Default::default()
929 },
930 )
931 })
932 }
933}
934
935pub(crate) fn insert_crease_for_image(
936 excerpt_id: ExcerptId,
937 anchor: text::Anchor,
938 content_len: usize,
939 abs_path: Option<Arc<Path>>,
940 editor: Entity<Editor>,
941 window: &mut Window,
942 cx: &mut App,
943) -> Option<CreaseId> {
944 let crease_label = abs_path
945 .as_ref()
946 .and_then(|path| path.file_name())
947 .map(|name| name.to_string_lossy().to_string().into())
948 .unwrap_or(SharedString::from("Image"));
949
950 editor.update(cx, |editor, cx| {
951 let snapshot = editor.buffer().read(cx).snapshot(cx);
952
953 let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?;
954
955 let start = start.bias_right(&snapshot);
956 let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
957
958 let placeholder = FoldPlaceholder {
959 render: render_image_fold_icon_button(crease_label, cx.weak_entity()),
960 merge_adjacent: false,
961 ..Default::default()
962 };
963
964 let crease = Crease::Inline {
965 range: start..end,
966 placeholder,
967 render_toggle: None,
968 render_trailer: None,
969 metadata: None,
970 };
971
972 let ids = editor.insert_creases(vec![crease.clone()], cx);
973 editor.fold_creases(vec![crease], false, window, cx);
974
975 Some(ids[0])
976 })
977}
978
979fn render_image_fold_icon_button(
980 label: SharedString,
981 editor: WeakEntity<Editor>,
982) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
983 Arc::new({
984 move |fold_id, fold_range, cx| {
985 let is_in_text_selection = editor
986 .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
987 .unwrap_or_default();
988
989 ButtonLike::new(fold_id)
990 .style(ButtonStyle::Filled)
991 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
992 .toggle_state(is_in_text_selection)
993 .child(
994 h_flex()
995 .gap_1()
996 .child(
997 Icon::new(IconName::Image)
998 .size(IconSize::XSmall)
999 .color(Color::Muted),
1000 )
1001 .child(
1002 Label::new(label.clone())
1003 .size(LabelSize::Small)
1004 .buffer_font(cx)
1005 .single_line(),
1006 ),
1007 )
1008 .into_any_element()
1009 }
1010 })
1011}
1012
1013#[derive(Debug, Eq, PartialEq)]
1014pub enum Mention {
1015 Text { uri: MentionUri, content: String },
1016 Image(MentionImage),
1017}
1018
1019#[derive(Clone, Debug, Eq, PartialEq)]
1020pub struct MentionImage {
1021 pub abs_path: Option<PathBuf>,
1022 pub data: SharedString,
1023 pub format: ImageFormat,
1024}
1025
1026#[derive(Default)]
1027pub struct MentionSet {
1028 uri_by_crease_id: HashMap<CreaseId, MentionUri>,
1029 fetch_results: HashMap<Url, Shared<Task<Result<String, String>>>>,
1030 images: HashMap<CreaseId, Shared<Task<Result<MentionImage, String>>>>,
1031 thread_summaries: HashMap<ThreadId, Shared<Task<Result<SharedString, String>>>>,
1032 text_thread_summaries: HashMap<PathBuf, Shared<Task<Result<String, String>>>>,
1033}
1034
1035impl MentionSet {
1036 pub fn insert_uri(&mut self, crease_id: CreaseId, uri: MentionUri) {
1037 self.uri_by_crease_id.insert(crease_id, uri);
1038 }
1039
1040 pub fn add_fetch_result(&mut self, url: Url, content: Shared<Task<Result<String, String>>>) {
1041 self.fetch_results.insert(url, content);
1042 }
1043
1044 pub fn insert_image(
1045 &mut self,
1046 crease_id: CreaseId,
1047 task: Shared<Task<Result<MentionImage, String>>>,
1048 ) {
1049 self.images.insert(crease_id, task);
1050 }
1051
1052 fn insert_thread(&mut self, id: ThreadId, task: Shared<Task<Result<SharedString, String>>>) {
1053 self.thread_summaries.insert(id, task);
1054 }
1055
1056 fn insert_text_thread(&mut self, path: PathBuf, task: Shared<Task<Result<String, String>>>) {
1057 self.text_thread_summaries.insert(path, task);
1058 }
1059
1060 pub fn drain(&mut self) -> impl Iterator<Item = CreaseId> {
1061 self.fetch_results.clear();
1062 self.thread_summaries.clear();
1063 self.text_thread_summaries.clear();
1064 self.uri_by_crease_id
1065 .drain()
1066 .map(|(id, _)| id)
1067 .chain(self.images.drain().map(|(id, _)| id))
1068 }
1069
1070 pub fn contents(
1071 &self,
1072 project: Entity<Project>,
1073 thread_store: Entity<ThreadStore>,
1074 _window: &mut Window,
1075 cx: &mut App,
1076 ) -> Task<Result<HashMap<CreaseId, Mention>>> {
1077 let mut processed_image_creases = HashSet::default();
1078
1079 let mut contents = self
1080 .uri_by_crease_id
1081 .iter()
1082 .map(|(&crease_id, uri)| {
1083 match uri {
1084 MentionUri::File { abs_path, .. } => {
1085 // TODO directories
1086 let uri = uri.clone();
1087 let abs_path = abs_path.to_path_buf();
1088
1089 if let Some(task) = self.images.get(&crease_id).cloned() {
1090 processed_image_creases.insert(crease_id);
1091 return cx.spawn(async move |_| {
1092 let image = task.await.map_err(|e| anyhow!("{e}"))?;
1093 anyhow::Ok((crease_id, Mention::Image(image)))
1094 });
1095 }
1096
1097 let buffer_task = project.update(cx, |project, cx| {
1098 let path = project
1099 .find_project_path(abs_path, cx)
1100 .context("Failed to find project path")?;
1101 anyhow::Ok(project.open_buffer(path, cx))
1102 });
1103 cx.spawn(async move |cx| {
1104 let buffer = buffer_task?.await?;
1105 let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
1106
1107 anyhow::Ok((crease_id, Mention::Text { uri, content }))
1108 })
1109 }
1110 MentionUri::Symbol {
1111 path, line_range, ..
1112 }
1113 | MentionUri::Selection {
1114 path, line_range, ..
1115 } => {
1116 let uri = uri.clone();
1117 let path_buf = path.clone();
1118 let line_range = line_range.clone();
1119
1120 let buffer_task = project.update(cx, |project, cx| {
1121 let path = project
1122 .find_project_path(&path_buf, cx)
1123 .context("Failed to find project path")?;
1124 anyhow::Ok(project.open_buffer(path, cx))
1125 });
1126
1127 cx.spawn(async move |cx| {
1128 let buffer = buffer_task?.await?;
1129 let content = buffer.read_with(cx, |buffer, _cx| {
1130 buffer
1131 .text_for_range(
1132 Point::new(line_range.start, 0)
1133 ..Point::new(
1134 line_range.end,
1135 buffer.line_len(line_range.end),
1136 ),
1137 )
1138 .collect()
1139 })?;
1140
1141 anyhow::Ok((crease_id, Mention::Text { uri, content }))
1142 })
1143 }
1144 MentionUri::Thread { id, .. } => {
1145 let Some(content) = self.thread_summaries.get(id).cloned() else {
1146 return Task::ready(Err(anyhow!("missing thread summary")));
1147 };
1148 let uri = uri.clone();
1149 cx.spawn(async move |_| {
1150 Ok((
1151 crease_id,
1152 Mention::Text {
1153 uri,
1154 content: content
1155 .await
1156 .map_err(|e| anyhow::anyhow!("{e}"))?
1157 .to_string(),
1158 },
1159 ))
1160 })
1161 }
1162 MentionUri::TextThread { path, .. } => {
1163 let Some(content) = self.text_thread_summaries.get(path).cloned() else {
1164 return Task::ready(Err(anyhow!("missing text thread summary")));
1165 };
1166 let uri = uri.clone();
1167 cx.spawn(async move |_| {
1168 Ok((
1169 crease_id,
1170 Mention::Text {
1171 uri,
1172 content: content
1173 .await
1174 .map_err(|e| anyhow::anyhow!("{e}"))?
1175 .to_string(),
1176 },
1177 ))
1178 })
1179 }
1180 MentionUri::Rule { id: prompt_id, .. } => {
1181 let Some(prompt_store) = thread_store.read(cx).prompt_store().clone()
1182 else {
1183 return Task::ready(Err(anyhow!("missing prompt store")));
1184 };
1185 let text_task = prompt_store.read(cx).load(*prompt_id, cx);
1186 let uri = uri.clone();
1187 cx.spawn(async move |_| {
1188 // TODO: report load errors instead of just logging
1189 let text = text_task.await?;
1190 anyhow::Ok((crease_id, Mention::Text { uri, content: text }))
1191 })
1192 }
1193 MentionUri::Fetch { url } => {
1194 let Some(content) = self.fetch_results.get(url).cloned() else {
1195 return Task::ready(Err(anyhow!("missing fetch result")));
1196 };
1197 let uri = uri.clone();
1198 cx.spawn(async move |_| {
1199 Ok((
1200 crease_id,
1201 Mention::Text {
1202 uri,
1203 content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?,
1204 },
1205 ))
1206 })
1207 }
1208 }
1209 })
1210 .collect::<Vec<_>>();
1211
1212 // Handle images that didn't have a mention URI (because they were added by the paste handler).
1213 contents.extend(self.images.iter().filter_map(|(crease_id, image)| {
1214 if processed_image_creases.contains(crease_id) {
1215 return None;
1216 }
1217 let crease_id = *crease_id;
1218 let image = image.clone();
1219 Some(cx.spawn(async move |_| {
1220 Ok((
1221 crease_id,
1222 Mention::Image(image.await.map_err(|e| anyhow::anyhow!("{e}"))?),
1223 ))
1224 }))
1225 }));
1226
1227 cx.spawn(async move |_cx| {
1228 let contents = try_join_all(contents).await?.into_iter().collect();
1229 anyhow::Ok(contents)
1230 })
1231 }
1232}
1233
1234#[cfg(test)]
1235mod tests {
1236 use std::{ops::Range, path::Path, sync::Arc};
1237
1238 use agent::{TextThreadStore, ThreadStore};
1239 use agent_client_protocol as acp;
1240 use editor::{AnchorRangeExt as _, Editor, EditorMode};
1241 use fs::FakeFs;
1242 use futures::StreamExt as _;
1243 use gpui::{
1244 AppContext, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext,
1245 };
1246 use lsp::{CompletionContext, CompletionTriggerKind};
1247 use project::{CompletionIntent, Project, ProjectPath};
1248 use serde_json::json;
1249 use text::Point;
1250 use ui::{App, Context, IntoElement, Render, SharedString, Window};
1251 use util::path;
1252 use workspace::{AppState, Item, Workspace};
1253
1254 use crate::acp::{
1255 message_editor::{Mention, MessageEditor},
1256 thread_view::tests::init_test,
1257 };
1258
1259 #[gpui::test]
1260 async fn test_at_mention_removal(cx: &mut TestAppContext) {
1261 init_test(cx);
1262
1263 let fs = FakeFs::new(cx.executor());
1264 fs.insert_tree("/project", json!({"file": ""})).await;
1265 let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
1266
1267 let (workspace, cx) =
1268 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1269
1270 let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx));
1271 let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
1272
1273 let message_editor = cx.update(|window, cx| {
1274 cx.new(|cx| {
1275 MessageEditor::new(
1276 workspace.downgrade(),
1277 project.clone(),
1278 thread_store.clone(),
1279 text_thread_store.clone(),
1280 "Test",
1281 EditorMode::AutoHeight {
1282 min_lines: 1,
1283 max_lines: None,
1284 },
1285 window,
1286 cx,
1287 )
1288 })
1289 });
1290 let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1291
1292 cx.run_until_parked();
1293
1294 let excerpt_id = editor.update(cx, |editor, cx| {
1295 editor
1296 .buffer()
1297 .read(cx)
1298 .excerpt_ids()
1299 .into_iter()
1300 .next()
1301 .unwrap()
1302 });
1303 let completions = editor.update_in(cx, |editor, window, cx| {
1304 editor.set_text("Hello @file ", window, cx);
1305 let buffer = editor.buffer().read(cx).as_singleton().unwrap();
1306 let completion_provider = editor.completion_provider().unwrap();
1307 completion_provider.completions(
1308 excerpt_id,
1309 &buffer,
1310 text::Anchor::MAX,
1311 CompletionContext {
1312 trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
1313 trigger_character: Some("@".into()),
1314 },
1315 window,
1316 cx,
1317 )
1318 });
1319 let [_, completion]: [_; 2] = completions
1320 .await
1321 .unwrap()
1322 .into_iter()
1323 .flat_map(|response| response.completions)
1324 .collect::<Vec<_>>()
1325 .try_into()
1326 .unwrap();
1327
1328 editor.update_in(cx, |editor, window, cx| {
1329 let snapshot = editor.buffer().read(cx).snapshot(cx);
1330 let start = snapshot
1331 .anchor_in_excerpt(excerpt_id, completion.replace_range.start)
1332 .unwrap();
1333 let end = snapshot
1334 .anchor_in_excerpt(excerpt_id, completion.replace_range.end)
1335 .unwrap();
1336 editor.edit([(start..end, completion.new_text)], cx);
1337 (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
1338 });
1339
1340 cx.run_until_parked();
1341
1342 // Backspace over the inserted crease (and the following space).
1343 editor.update_in(cx, |editor, window, cx| {
1344 editor.backspace(&Default::default(), window, cx);
1345 editor.backspace(&Default::default(), window, cx);
1346 });
1347
1348 let content = message_editor
1349 .update_in(cx, |message_editor, window, cx| {
1350 message_editor.contents(window, cx)
1351 })
1352 .await
1353 .unwrap();
1354
1355 // We don't send a resource link for the deleted crease.
1356 pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
1357 }
1358
1359 struct MessageEditorItem(Entity<MessageEditor>);
1360
1361 impl Item for MessageEditorItem {
1362 type Event = ();
1363
1364 fn include_in_nav_history() -> bool {
1365 false
1366 }
1367
1368 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1369 "Test".into()
1370 }
1371 }
1372
1373 impl EventEmitter<()> for MessageEditorItem {}
1374
1375 impl Focusable for MessageEditorItem {
1376 fn focus_handle(&self, cx: &App) -> FocusHandle {
1377 self.0.read(cx).focus_handle(cx).clone()
1378 }
1379 }
1380
1381 impl Render for MessageEditorItem {
1382 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1383 self.0.clone().into_any_element()
1384 }
1385 }
1386
1387 #[gpui::test]
1388 async fn test_context_completion_provider(cx: &mut TestAppContext) {
1389 init_test(cx);
1390
1391 let app_state = cx.update(AppState::test);
1392
1393 cx.update(|cx| {
1394 language::init(cx);
1395 editor::init(cx);
1396 workspace::init(app_state.clone(), cx);
1397 Project::init_settings(cx);
1398 });
1399
1400 app_state
1401 .fs
1402 .as_fake()
1403 .insert_tree(
1404 path!("/dir"),
1405 json!({
1406 "editor": "",
1407 "a": {
1408 "one.txt": "1",
1409 "two.txt": "2",
1410 "three.txt": "3",
1411 "four.txt": "4"
1412 },
1413 "b": {
1414 "five.txt": "5",
1415 "six.txt": "6",
1416 "seven.txt": "7",
1417 "eight.txt": "8",
1418 }
1419 }),
1420 )
1421 .await;
1422
1423 let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
1424 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1425 let workspace = window.root(cx).unwrap();
1426
1427 let worktree = project.update(cx, |project, cx| {
1428 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
1429 assert_eq!(worktrees.len(), 1);
1430 worktrees.pop().unwrap()
1431 });
1432 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1433
1434 let mut cx = VisualTestContext::from_window(*window, cx);
1435
1436 let paths = vec![
1437 path!("a/one.txt"),
1438 path!("a/two.txt"),
1439 path!("a/three.txt"),
1440 path!("a/four.txt"),
1441 path!("b/five.txt"),
1442 path!("b/six.txt"),
1443 path!("b/seven.txt"),
1444 path!("b/eight.txt"),
1445 ];
1446
1447 let mut opened_editors = Vec::new();
1448 for path in paths {
1449 let buffer = workspace
1450 .update_in(&mut cx, |workspace, window, cx| {
1451 workspace.open_path(
1452 ProjectPath {
1453 worktree_id,
1454 path: Path::new(path).into(),
1455 },
1456 None,
1457 false,
1458 window,
1459 cx,
1460 )
1461 })
1462 .await
1463 .unwrap();
1464 opened_editors.push(buffer);
1465 }
1466
1467 let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx));
1468 let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
1469
1470 let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
1471 let workspace_handle = cx.weak_entity();
1472 let message_editor = cx.new(|cx| {
1473 MessageEditor::new(
1474 workspace_handle,
1475 project.clone(),
1476 thread_store.clone(),
1477 text_thread_store.clone(),
1478 "Test",
1479 EditorMode::AutoHeight {
1480 max_lines: None,
1481 min_lines: 1,
1482 },
1483 window,
1484 cx,
1485 )
1486 });
1487 workspace.active_pane().update(cx, |pane, cx| {
1488 pane.add_item(
1489 Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
1490 true,
1491 true,
1492 None,
1493 window,
1494 cx,
1495 );
1496 });
1497 message_editor.read(cx).focus_handle(cx).focus(window);
1498 let editor = message_editor.read(cx).editor().clone();
1499 (message_editor, editor)
1500 });
1501
1502 cx.simulate_input("Lorem ");
1503
1504 editor.update(&mut cx, |editor, cx| {
1505 assert_eq!(editor.text(cx), "Lorem ");
1506 assert!(!editor.has_visible_completions_menu());
1507 });
1508
1509 cx.simulate_input("@");
1510
1511 editor.update(&mut cx, |editor, cx| {
1512 assert_eq!(editor.text(cx), "Lorem @");
1513 assert!(editor.has_visible_completions_menu());
1514 assert_eq!(
1515 current_completion_labels(editor),
1516 &[
1517 "eight.txt dir/b/",
1518 "seven.txt dir/b/",
1519 "six.txt dir/b/",
1520 "five.txt dir/b/",
1521 "Files & Directories",
1522 "Symbols",
1523 "Threads",
1524 "Fetch"
1525 ]
1526 );
1527 });
1528
1529 // Select and confirm "File"
1530 editor.update_in(&mut cx, |editor, window, cx| {
1531 assert!(editor.has_visible_completions_menu());
1532 editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1533 editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1534 editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1535 editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1536 editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1537 });
1538
1539 cx.run_until_parked();
1540
1541 editor.update(&mut cx, |editor, cx| {
1542 assert_eq!(editor.text(cx), "Lorem @file ");
1543 assert!(editor.has_visible_completions_menu());
1544 });
1545
1546 cx.simulate_input("one");
1547
1548 editor.update(&mut cx, |editor, cx| {
1549 assert_eq!(editor.text(cx), "Lorem @file one");
1550 assert!(editor.has_visible_completions_menu());
1551 assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
1552 });
1553
1554 editor.update_in(&mut cx, |editor, window, cx| {
1555 assert!(editor.has_visible_completions_menu());
1556 editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1557 });
1558
1559 editor.update(&mut cx, |editor, cx| {
1560 assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) ");
1561 assert!(!editor.has_visible_completions_menu());
1562 assert_eq!(
1563 fold_ranges(editor, cx),
1564 vec![Point::new(0, 6)..Point::new(0, 39)]
1565 );
1566 });
1567
1568 let contents = message_editor
1569 .update_in(&mut cx, |message_editor, window, cx| {
1570 message_editor.mention_set().contents(
1571 project.clone(),
1572 thread_store.clone(),
1573 window,
1574 cx,
1575 )
1576 })
1577 .await
1578 .unwrap()
1579 .into_values()
1580 .collect::<Vec<_>>();
1581
1582 pretty_assertions::assert_eq!(
1583 contents,
1584 [Mention::Text {
1585 content: "1".into(),
1586 uri: "file:///dir/a/one.txt".parse().unwrap()
1587 }]
1588 );
1589
1590 cx.simulate_input(" ");
1591
1592 editor.update(&mut cx, |editor, cx| {
1593 assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) ");
1594 assert!(!editor.has_visible_completions_menu());
1595 assert_eq!(
1596 fold_ranges(editor, cx),
1597 vec![Point::new(0, 6)..Point::new(0, 39)]
1598 );
1599 });
1600
1601 cx.simulate_input("Ipsum ");
1602
1603 editor.update(&mut cx, |editor, cx| {
1604 assert_eq!(
1605 editor.text(cx),
1606 "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum ",
1607 );
1608 assert!(!editor.has_visible_completions_menu());
1609 assert_eq!(
1610 fold_ranges(editor, cx),
1611 vec![Point::new(0, 6)..Point::new(0, 39)]
1612 );
1613 });
1614
1615 cx.simulate_input("@file ");
1616
1617 editor.update(&mut cx, |editor, cx| {
1618 assert_eq!(
1619 editor.text(cx),
1620 "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum @file ",
1621 );
1622 assert!(editor.has_visible_completions_menu());
1623 assert_eq!(
1624 fold_ranges(editor, cx),
1625 vec![Point::new(0, 6)..Point::new(0, 39)]
1626 );
1627 });
1628
1629 editor.update_in(&mut cx, |editor, window, cx| {
1630 editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1631 });
1632
1633 cx.run_until_parked();
1634
1635 let contents = message_editor
1636 .update_in(&mut cx, |message_editor, window, cx| {
1637 message_editor.mention_set().contents(
1638 project.clone(),
1639 thread_store.clone(),
1640 window,
1641 cx,
1642 )
1643 })
1644 .await
1645 .unwrap()
1646 .into_values()
1647 .collect::<Vec<_>>();
1648
1649 assert_eq!(contents.len(), 2);
1650 pretty_assertions::assert_eq!(
1651 contents[1],
1652 Mention::Text {
1653 content: "8".to_string(),
1654 uri: "file:///dir/b/eight.txt".parse().unwrap(),
1655 }
1656 );
1657
1658 editor.update(&mut cx, |editor, cx| {
1659 assert_eq!(
1660 editor.text(cx),
1661 "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) "
1662 );
1663 assert!(!editor.has_visible_completions_menu());
1664 assert_eq!(
1665 fold_ranges(editor, cx),
1666 vec![
1667 Point::new(0, 6)..Point::new(0, 39),
1668 Point::new(0, 47)..Point::new(0, 84)
1669 ]
1670 );
1671 });
1672
1673 let plain_text_language = Arc::new(language::Language::new(
1674 language::LanguageConfig {
1675 name: "Plain Text".into(),
1676 matcher: language::LanguageMatcher {
1677 path_suffixes: vec!["txt".to_string()],
1678 ..Default::default()
1679 },
1680 ..Default::default()
1681 },
1682 None,
1683 ));
1684
1685 // Register the language and fake LSP
1686 let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
1687 language_registry.add(plain_text_language);
1688
1689 let mut fake_language_servers = language_registry.register_fake_lsp(
1690 "Plain Text",
1691 language::FakeLspAdapter {
1692 capabilities: lsp::ServerCapabilities {
1693 workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
1694 ..Default::default()
1695 },
1696 ..Default::default()
1697 },
1698 );
1699
1700 // Open the buffer to trigger LSP initialization
1701 let buffer = project
1702 .update(&mut cx, |project, cx| {
1703 project.open_local_buffer(path!("/dir/a/one.txt"), cx)
1704 })
1705 .await
1706 .unwrap();
1707
1708 // Register the buffer with language servers
1709 let _handle = project.update(&mut cx, |project, cx| {
1710 project.register_buffer_with_language_servers(&buffer, cx)
1711 });
1712
1713 cx.run_until_parked();
1714
1715 let fake_language_server = fake_language_servers.next().await.unwrap();
1716 fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
1717 |_, _| async move {
1718 Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
1719 #[allow(deprecated)]
1720 lsp::SymbolInformation {
1721 name: "MySymbol".into(),
1722 location: lsp::Location {
1723 uri: lsp::Url::from_file_path(path!("/dir/a/one.txt")).unwrap(),
1724 range: lsp::Range::new(
1725 lsp::Position::new(0, 0),
1726 lsp::Position::new(0, 1),
1727 ),
1728 },
1729 kind: lsp::SymbolKind::CONSTANT,
1730 tags: None,
1731 container_name: None,
1732 deprecated: None,
1733 },
1734 ])))
1735 },
1736 );
1737
1738 cx.simulate_input("@symbol ");
1739
1740 editor.update(&mut cx, |editor, cx| {
1741 assert_eq!(
1742 editor.text(cx),
1743 "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) @symbol "
1744 );
1745 assert!(editor.has_visible_completions_menu());
1746 assert_eq!(
1747 current_completion_labels(editor),
1748 &[
1749 "MySymbol",
1750 ]
1751 );
1752 });
1753
1754 editor.update_in(&mut cx, |editor, window, cx| {
1755 editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1756 });
1757
1758 let contents = message_editor
1759 .update_in(&mut cx, |message_editor, window, cx| {
1760 message_editor
1761 .mention_set()
1762 .contents(project.clone(), thread_store, window, cx)
1763 })
1764 .await
1765 .unwrap()
1766 .into_values()
1767 .collect::<Vec<_>>();
1768
1769 assert_eq!(contents.len(), 3);
1770 pretty_assertions::assert_eq!(
1771 contents[2],
1772 Mention::Text {
1773 content: "1".into(),
1774 uri: "file:///dir/a/one.txt?symbol=MySymbol#L1:1"
1775 .parse()
1776 .unwrap(),
1777 }
1778 );
1779
1780 cx.run_until_parked();
1781
1782 editor.read_with(&mut cx, |editor, cx| {
1783 assert_eq!(
1784 editor.text(cx),
1785 "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) [@MySymbol](file:///dir/a/one.txt?symbol=MySymbol#L1:1) "
1786 );
1787 });
1788 }
1789
1790 fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
1791 let snapshot = editor.buffer().read(cx).snapshot(cx);
1792 editor.display_map.update(cx, |display_map, cx| {
1793 display_map
1794 .snapshot(cx)
1795 .folds_in_range(0..snapshot.len())
1796 .map(|fold| fold.range.to_point(&snapshot))
1797 .collect()
1798 })
1799 }
1800
1801 fn current_completion_labels(editor: &Editor) -> Vec<String> {
1802 let completions = editor.current_completions().expect("Missing completions");
1803 completions
1804 .into_iter()
1805 .map(|completion| completion.label.text.to_string())
1806 .collect::<Vec<_>>()
1807 }
1808}