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