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