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