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