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