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