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