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