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