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