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