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