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