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