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