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