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