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.clone(),
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.map_err(|e| e.to_string())?;
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 let image_task = image_task.clone();
1235 move |fold_id, fold_range, cx| {
1236 let is_in_text_selection = editor
1237 .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
1238 .unwrap_or_default();
1239
1240 ButtonLike::new(fold_id)
1241 .style(ButtonStyle::Filled)
1242 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
1243 .toggle_state(is_in_text_selection)
1244 .child(
1245 h_flex()
1246 .gap_1()
1247 .child(
1248 Icon::new(IconName::Image)
1249 .size(IconSize::XSmall)
1250 .color(Color::Muted),
1251 )
1252 .child(
1253 Label::new(label.clone())
1254 .size(LabelSize::Small)
1255 .buffer_font(cx)
1256 .single_line(),
1257 ),
1258 )
1259 .hoverable_tooltip({
1260 let image_task = image_task.clone();
1261 move |_, cx| {
1262 let image = image_task.peek().cloned().transpose().ok().flatten();
1263 let image_task = image_task.clone();
1264 cx.new::<ImageHover>(|cx| ImageHover {
1265 image,
1266 _task: cx.spawn(async move |this, cx| {
1267 if let Ok(image) = image_task.clone().await {
1268 this.update(cx, |this, cx| {
1269 if this.image.replace(image).is_none() {
1270 cx.notify();
1271 }
1272 })
1273 .ok();
1274 }
1275 }),
1276 })
1277 .into()
1278 }
1279 })
1280 .into_any_element()
1281 }
1282 })
1283}
1284
1285struct ImageHover {
1286 image: Option<Arc<Image>>,
1287 _task: Task<()>,
1288}
1289
1290impl Render for ImageHover {
1291 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1292 if let Some(image) = self.image.clone() {
1293 gpui::img(image).max_w_96().max_h_96().into_any_element()
1294 } else {
1295 gpui::Empty.into_any_element()
1296 }
1297 }
1298}
1299
1300#[derive(Debug, Eq, PartialEq)]
1301pub enum Mention {
1302 Text { uri: MentionUri, content: String },
1303 Image(MentionImage),
1304}
1305
1306#[derive(Clone, Debug, Eq, PartialEq)]
1307pub struct MentionImage {
1308 pub abs_path: Option<PathBuf>,
1309 pub data: SharedString,
1310 pub format: ImageFormat,
1311}
1312
1313#[derive(Default)]
1314pub struct MentionSet {
1315 uri_by_crease_id: HashMap<CreaseId, MentionUri>,
1316 fetch_results: HashMap<Url, Shared<Task<Result<String, String>>>>,
1317 images: HashMap<CreaseId, Shared<Task<Result<MentionImage, String>>>>,
1318 thread_summaries: HashMap<acp::SessionId, Shared<Task<Result<SharedString, String>>>>,
1319 text_thread_summaries: HashMap<PathBuf, Shared<Task<Result<String, String>>>>,
1320 directories: HashMap<PathBuf, Shared<Task<Result<String, String>>>>,
1321}
1322
1323impl MentionSet {
1324 pub fn insert_uri(&mut self, crease_id: CreaseId, uri: MentionUri) {
1325 self.uri_by_crease_id.insert(crease_id, uri);
1326 }
1327
1328 pub fn add_fetch_result(&mut self, url: Url, content: Shared<Task<Result<String, String>>>) {
1329 self.fetch_results.insert(url, content);
1330 }
1331
1332 pub fn insert_image(
1333 &mut self,
1334 crease_id: CreaseId,
1335 task: Shared<Task<Result<MentionImage, String>>>,
1336 ) {
1337 self.images.insert(crease_id, task);
1338 }
1339
1340 fn insert_thread(
1341 &mut self,
1342 id: acp::SessionId,
1343 task: Shared<Task<Result<SharedString, String>>>,
1344 ) {
1345 self.thread_summaries.insert(id, task);
1346 }
1347
1348 fn insert_text_thread(&mut self, path: PathBuf, task: Shared<Task<Result<String, String>>>) {
1349 self.text_thread_summaries.insert(path, task);
1350 }
1351
1352 pub fn drain(&mut self) -> impl Iterator<Item = CreaseId> {
1353 self.fetch_results.clear();
1354 self.thread_summaries.clear();
1355 self.text_thread_summaries.clear();
1356 self.uri_by_crease_id
1357 .drain()
1358 .map(|(id, _)| id)
1359 .chain(self.images.drain().map(|(id, _)| id))
1360 }
1361
1362 pub fn contents(
1363 &self,
1364 project: &Entity<Project>,
1365 prompt_store: Option<&Entity<PromptStore>>,
1366 _window: &mut Window,
1367 cx: &mut App,
1368 ) -> Task<Result<HashMap<CreaseId, Mention>>> {
1369 let mut processed_image_creases = HashSet::default();
1370
1371 let mut contents = self
1372 .uri_by_crease_id
1373 .iter()
1374 .map(|(&crease_id, uri)| {
1375 match uri {
1376 MentionUri::File { abs_path, .. } => {
1377 let uri = uri.clone();
1378 let abs_path = abs_path.to_path_buf();
1379
1380 if let Some(task) = self.images.get(&crease_id).cloned() {
1381 processed_image_creases.insert(crease_id);
1382 return cx.spawn(async move |_| {
1383 let image = task.await.map_err(|e| anyhow!("{e}"))?;
1384 anyhow::Ok((crease_id, Mention::Image(image)))
1385 });
1386 }
1387
1388 let buffer_task = project.update(cx, |project, cx| {
1389 let path = project
1390 .find_project_path(abs_path, cx)
1391 .context("Failed to find project path")?;
1392 anyhow::Ok(project.open_buffer(path, cx))
1393 });
1394 cx.spawn(async move |cx| {
1395 let buffer = buffer_task?.await?;
1396 let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
1397
1398 anyhow::Ok((crease_id, Mention::Text { uri, content }))
1399 })
1400 }
1401 MentionUri::Directory { abs_path } => {
1402 let Some(content) = self.directories.get(abs_path).cloned() else {
1403 return Task::ready(Err(anyhow!("missing directory load task")));
1404 };
1405 let uri = uri.clone();
1406 cx.spawn(async move |_| {
1407 Ok((
1408 crease_id,
1409 Mention::Text {
1410 uri,
1411 content: content
1412 .await
1413 .map_err(|e| anyhow::anyhow!("{e}"))?
1414 .to_string(),
1415 },
1416 ))
1417 })
1418 }
1419 MentionUri::Symbol {
1420 path, line_range, ..
1421 }
1422 | MentionUri::Selection {
1423 path, line_range, ..
1424 } => {
1425 let uri = uri.clone();
1426 let path_buf = path.clone();
1427 let line_range = line_range.clone();
1428
1429 let buffer_task = project.update(cx, |project, cx| {
1430 let path = project
1431 .find_project_path(&path_buf, cx)
1432 .context("Failed to find project path")?;
1433 anyhow::Ok(project.open_buffer(path, cx))
1434 });
1435
1436 cx.spawn(async move |cx| {
1437 let buffer = buffer_task?.await?;
1438 let content = buffer.read_with(cx, |buffer, _cx| {
1439 buffer
1440 .text_for_range(
1441 Point::new(line_range.start, 0)
1442 ..Point::new(
1443 line_range.end,
1444 buffer.line_len(line_range.end),
1445 ),
1446 )
1447 .collect()
1448 })?;
1449
1450 anyhow::Ok((crease_id, Mention::Text { uri, content }))
1451 })
1452 }
1453 MentionUri::Thread { id, .. } => {
1454 let Some(content) = self.thread_summaries.get(id).cloned() else {
1455 return Task::ready(Err(anyhow!("missing thread summary")));
1456 };
1457 let uri = uri.clone();
1458 cx.spawn(async move |_| {
1459 Ok((
1460 crease_id,
1461 Mention::Text {
1462 uri,
1463 content: content
1464 .await
1465 .map_err(|e| anyhow::anyhow!("{e}"))?
1466 .to_string(),
1467 },
1468 ))
1469 })
1470 }
1471 MentionUri::TextThread { path, .. } => {
1472 let Some(content) = self.text_thread_summaries.get(path).cloned() else {
1473 return Task::ready(Err(anyhow!("missing text thread summary")));
1474 };
1475 let uri = uri.clone();
1476 cx.spawn(async move |_| {
1477 Ok((
1478 crease_id,
1479 Mention::Text {
1480 uri,
1481 content: content
1482 .await
1483 .map_err(|e| anyhow::anyhow!("{e}"))?
1484 .to_string(),
1485 },
1486 ))
1487 })
1488 }
1489 MentionUri::Rule { id: prompt_id, .. } => {
1490 let Some(prompt_store) = prompt_store else {
1491 return Task::ready(Err(anyhow!("missing prompt store")));
1492 };
1493 let text_task = prompt_store.read(cx).load(*prompt_id, cx);
1494 let uri = uri.clone();
1495 cx.spawn(async move |_| {
1496 // TODO: report load errors instead of just logging
1497 let text = text_task.await?;
1498 anyhow::Ok((crease_id, Mention::Text { uri, content: text }))
1499 })
1500 }
1501 MentionUri::Fetch { url } => {
1502 let Some(content) = self.fetch_results.get(url).cloned() else {
1503 return Task::ready(Err(anyhow!("missing fetch result")));
1504 };
1505 let uri = uri.clone();
1506 cx.spawn(async move |_| {
1507 Ok((
1508 crease_id,
1509 Mention::Text {
1510 uri,
1511 content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?,
1512 },
1513 ))
1514 })
1515 }
1516 }
1517 })
1518 .collect::<Vec<_>>();
1519
1520 // Handle images that didn't have a mention URI (because they were added by the paste handler).
1521 contents.extend(self.images.iter().filter_map(|(crease_id, image)| {
1522 if processed_image_creases.contains(crease_id) {
1523 return None;
1524 }
1525 let crease_id = *crease_id;
1526 let image = image.clone();
1527 Some(cx.spawn(async move |_| {
1528 Ok((
1529 crease_id,
1530 Mention::Image(image.await.map_err(|e| anyhow::anyhow!("{e}"))?),
1531 ))
1532 }))
1533 }));
1534
1535 cx.spawn(async move |_cx| {
1536 let contents = try_join_all(contents).await?.into_iter().collect();
1537 anyhow::Ok(contents)
1538 })
1539 }
1540}
1541
1542struct SlashCommandSemanticsProvider {
1543 range: Cell<Option<(usize, usize)>>,
1544}
1545
1546impl SemanticsProvider for SlashCommandSemanticsProvider {
1547 fn hover(
1548 &self,
1549 buffer: &Entity<Buffer>,
1550 position: text::Anchor,
1551 cx: &mut App,
1552 ) -> Option<Task<Vec<project::Hover>>> {
1553 let snapshot = buffer.read(cx).snapshot();
1554 let offset = position.to_offset(&snapshot);
1555 let (start, end) = self.range.get()?;
1556 if !(start..end).contains(&offset) {
1557 return None;
1558 }
1559 let range = snapshot.anchor_after(start)..snapshot.anchor_after(end);
1560 Some(Task::ready(vec![project::Hover {
1561 contents: vec![project::HoverBlock {
1562 text: "Slash commands are not supported".into(),
1563 kind: project::HoverBlockKind::PlainText,
1564 }],
1565 range: Some(range),
1566 language: None,
1567 }]))
1568 }
1569
1570 fn inline_values(
1571 &self,
1572 _buffer_handle: Entity<Buffer>,
1573 _range: Range<text::Anchor>,
1574 _cx: &mut App,
1575 ) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
1576 None
1577 }
1578
1579 fn inlay_hints(
1580 &self,
1581 _buffer_handle: Entity<Buffer>,
1582 _range: Range<text::Anchor>,
1583 _cx: &mut App,
1584 ) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
1585 None
1586 }
1587
1588 fn resolve_inlay_hint(
1589 &self,
1590 _hint: project::InlayHint,
1591 _buffer_handle: Entity<Buffer>,
1592 _server_id: lsp::LanguageServerId,
1593 _cx: &mut App,
1594 ) -> Option<Task<anyhow::Result<project::InlayHint>>> {
1595 None
1596 }
1597
1598 fn supports_inlay_hints(&self, _buffer: &Entity<Buffer>, _cx: &mut App) -> bool {
1599 false
1600 }
1601
1602 fn document_highlights(
1603 &self,
1604 _buffer: &Entity<Buffer>,
1605 _position: text::Anchor,
1606 _cx: &mut App,
1607 ) -> Option<Task<Result<Vec<project::DocumentHighlight>>>> {
1608 None
1609 }
1610
1611 fn definitions(
1612 &self,
1613 _buffer: &Entity<Buffer>,
1614 _position: text::Anchor,
1615 _kind: editor::GotoDefinitionKind,
1616 _cx: &mut App,
1617 ) -> Option<Task<Result<Vec<project::LocationLink>>>> {
1618 None
1619 }
1620
1621 fn range_for_rename(
1622 &self,
1623 _buffer: &Entity<Buffer>,
1624 _position: text::Anchor,
1625 _cx: &mut App,
1626 ) -> Option<Task<Result<Option<Range<text::Anchor>>>>> {
1627 None
1628 }
1629
1630 fn perform_rename(
1631 &self,
1632 _buffer: &Entity<Buffer>,
1633 _position: text::Anchor,
1634 _new_name: String,
1635 _cx: &mut App,
1636 ) -> Option<Task<Result<project::ProjectTransaction>>> {
1637 None
1638 }
1639}
1640
1641fn parse_slash_command(text: &str) -> Option<(usize, usize)> {
1642 if let Some(remainder) = text.strip_prefix('/') {
1643 let pos = remainder
1644 .find(char::is_whitespace)
1645 .unwrap_or(remainder.len());
1646 let command = &remainder[..pos];
1647 if !command.is_empty() && command.chars().all(char::is_alphanumeric) {
1648 return Some((0, 1 + command.len()));
1649 }
1650 }
1651 None
1652}
1653
1654pub struct MessageEditorAddon {}
1655
1656impl MessageEditorAddon {
1657 pub fn new() -> Self {
1658 Self {}
1659 }
1660}
1661
1662impl Addon for MessageEditorAddon {
1663 fn to_any(&self) -> &dyn std::any::Any {
1664 self
1665 }
1666
1667 fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
1668 Some(self)
1669 }
1670
1671 fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) {
1672 let settings = agent_settings::AgentSettings::get_global(cx);
1673 if settings.use_modifier_to_send {
1674 key_context.add("use_modifier_to_send");
1675 }
1676 }
1677}
1678
1679#[cfg(test)]
1680mod tests {
1681 use std::{ops::Range, path::Path, sync::Arc};
1682
1683 use agent_client_protocol as acp;
1684 use agent2::HistoryStore;
1685 use assistant_context::ContextStore;
1686 use editor::{AnchorRangeExt as _, Editor, EditorMode};
1687 use fs::FakeFs;
1688 use futures::StreamExt as _;
1689 use gpui::{
1690 AppContext, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext,
1691 };
1692 use lsp::{CompletionContext, CompletionTriggerKind};
1693 use project::{CompletionIntent, Project, ProjectPath};
1694 use serde_json::json;
1695 use text::Point;
1696 use ui::{App, Context, IntoElement, Render, SharedString, Window};
1697 use util::{path, uri};
1698 use workspace::{AppState, Item, Workspace};
1699
1700 use crate::acp::{
1701 message_editor::{Mention, MessageEditor},
1702 thread_view::tests::init_test,
1703 };
1704
1705 #[gpui::test]
1706 async fn test_at_mention_removal(cx: &mut TestAppContext) {
1707 init_test(cx);
1708
1709 let fs = FakeFs::new(cx.executor());
1710 fs.insert_tree("/project", json!({"file": ""})).await;
1711 let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
1712
1713 let (workspace, cx) =
1714 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
1715
1716 let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
1717 let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
1718
1719 let message_editor = cx.update(|window, cx| {
1720 cx.new(|cx| {
1721 MessageEditor::new(
1722 workspace.downgrade(),
1723 project.clone(),
1724 history_store.clone(),
1725 None,
1726 "Test",
1727 false,
1728 EditorMode::AutoHeight {
1729 min_lines: 1,
1730 max_lines: None,
1731 },
1732 window,
1733 cx,
1734 )
1735 })
1736 });
1737 let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
1738
1739 cx.run_until_parked();
1740
1741 let excerpt_id = editor.update(cx, |editor, cx| {
1742 editor
1743 .buffer()
1744 .read(cx)
1745 .excerpt_ids()
1746 .into_iter()
1747 .next()
1748 .unwrap()
1749 });
1750 let completions = editor.update_in(cx, |editor, window, cx| {
1751 editor.set_text("Hello @file ", window, cx);
1752 let buffer = editor.buffer().read(cx).as_singleton().unwrap();
1753 let completion_provider = editor.completion_provider().unwrap();
1754 completion_provider.completions(
1755 excerpt_id,
1756 &buffer,
1757 text::Anchor::MAX,
1758 CompletionContext {
1759 trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
1760 trigger_character: Some("@".into()),
1761 },
1762 window,
1763 cx,
1764 )
1765 });
1766 let [_, completion]: [_; 2] = completions
1767 .await
1768 .unwrap()
1769 .into_iter()
1770 .flat_map(|response| response.completions)
1771 .collect::<Vec<_>>()
1772 .try_into()
1773 .unwrap();
1774
1775 editor.update_in(cx, |editor, window, cx| {
1776 let snapshot = editor.buffer().read(cx).snapshot(cx);
1777 let start = snapshot
1778 .anchor_in_excerpt(excerpt_id, completion.replace_range.start)
1779 .unwrap();
1780 let end = snapshot
1781 .anchor_in_excerpt(excerpt_id, completion.replace_range.end)
1782 .unwrap();
1783 editor.edit([(start..end, completion.new_text)], cx);
1784 (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
1785 });
1786
1787 cx.run_until_parked();
1788
1789 // Backspace over the inserted crease (and the following space).
1790 editor.update_in(cx, |editor, window, cx| {
1791 editor.backspace(&Default::default(), window, cx);
1792 editor.backspace(&Default::default(), window, cx);
1793 });
1794
1795 let content = message_editor
1796 .update_in(cx, |message_editor, window, cx| {
1797 message_editor.contents(window, cx)
1798 })
1799 .await
1800 .unwrap();
1801
1802 // We don't send a resource link for the deleted crease.
1803 pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
1804 }
1805
1806 struct MessageEditorItem(Entity<MessageEditor>);
1807
1808 impl Item for MessageEditorItem {
1809 type Event = ();
1810
1811 fn include_in_nav_history() -> bool {
1812 false
1813 }
1814
1815 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1816 "Test".into()
1817 }
1818 }
1819
1820 impl EventEmitter<()> for MessageEditorItem {}
1821
1822 impl Focusable for MessageEditorItem {
1823 fn focus_handle(&self, cx: &App) -> FocusHandle {
1824 self.0.read(cx).focus_handle(cx).clone()
1825 }
1826 }
1827
1828 impl Render for MessageEditorItem {
1829 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1830 self.0.clone().into_any_element()
1831 }
1832 }
1833
1834 #[gpui::test]
1835 async fn test_context_completion_provider(cx: &mut TestAppContext) {
1836 init_test(cx);
1837
1838 let app_state = cx.update(AppState::test);
1839
1840 cx.update(|cx| {
1841 language::init(cx);
1842 editor::init(cx);
1843 workspace::init(app_state.clone(), cx);
1844 Project::init_settings(cx);
1845 });
1846
1847 app_state
1848 .fs
1849 .as_fake()
1850 .insert_tree(
1851 path!("/dir"),
1852 json!({
1853 "editor": "",
1854 "a": {
1855 "one.txt": "1",
1856 "two.txt": "2",
1857 "three.txt": "3",
1858 "four.txt": "4"
1859 },
1860 "b": {
1861 "five.txt": "5",
1862 "six.txt": "6",
1863 "seven.txt": "7",
1864 "eight.txt": "8",
1865 }
1866 }),
1867 )
1868 .await;
1869
1870 let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
1871 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1872 let workspace = window.root(cx).unwrap();
1873
1874 let worktree = project.update(cx, |project, cx| {
1875 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
1876 assert_eq!(worktrees.len(), 1);
1877 worktrees.pop().unwrap()
1878 });
1879 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1880
1881 let mut cx = VisualTestContext::from_window(*window, cx);
1882
1883 let paths = vec![
1884 path!("a/one.txt"),
1885 path!("a/two.txt"),
1886 path!("a/three.txt"),
1887 path!("a/four.txt"),
1888 path!("b/five.txt"),
1889 path!("b/six.txt"),
1890 path!("b/seven.txt"),
1891 path!("b/eight.txt"),
1892 ];
1893
1894 let mut opened_editors = Vec::new();
1895 for path in paths {
1896 let buffer = workspace
1897 .update_in(&mut cx, |workspace, window, cx| {
1898 workspace.open_path(
1899 ProjectPath {
1900 worktree_id,
1901 path: Path::new(path).into(),
1902 },
1903 None,
1904 false,
1905 window,
1906 cx,
1907 )
1908 })
1909 .await
1910 .unwrap();
1911 opened_editors.push(buffer);
1912 }
1913
1914 let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
1915 let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
1916
1917 let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
1918 let workspace_handle = cx.weak_entity();
1919 let message_editor = cx.new(|cx| {
1920 MessageEditor::new(
1921 workspace_handle,
1922 project.clone(),
1923 history_store.clone(),
1924 None,
1925 "Test",
1926 false,
1927 EditorMode::AutoHeight {
1928 max_lines: None,
1929 min_lines: 1,
1930 },
1931 window,
1932 cx,
1933 )
1934 });
1935 workspace.active_pane().update(cx, |pane, cx| {
1936 pane.add_item(
1937 Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
1938 true,
1939 true,
1940 None,
1941 window,
1942 cx,
1943 );
1944 });
1945 message_editor.read(cx).focus_handle(cx).focus(window);
1946 let editor = message_editor.read(cx).editor().clone();
1947 (message_editor, editor)
1948 });
1949
1950 cx.simulate_input("Lorem ");
1951
1952 editor.update(&mut cx, |editor, cx| {
1953 assert_eq!(editor.text(cx), "Lorem ");
1954 assert!(!editor.has_visible_completions_menu());
1955 });
1956
1957 cx.simulate_input("@");
1958
1959 editor.update(&mut cx, |editor, cx| {
1960 assert_eq!(editor.text(cx), "Lorem @");
1961 assert!(editor.has_visible_completions_menu());
1962 assert_eq!(
1963 current_completion_labels(editor),
1964 &[
1965 "eight.txt dir/b/",
1966 "seven.txt dir/b/",
1967 "six.txt dir/b/",
1968 "five.txt dir/b/",
1969 "Files & Directories",
1970 "Symbols",
1971 "Threads",
1972 "Fetch"
1973 ]
1974 );
1975 });
1976
1977 // Select and confirm "File"
1978 editor.update_in(&mut cx, |editor, window, cx| {
1979 assert!(editor.has_visible_completions_menu());
1980 editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1981 editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1982 editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1983 editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1984 editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1985 });
1986
1987 cx.run_until_parked();
1988
1989 editor.update(&mut cx, |editor, cx| {
1990 assert_eq!(editor.text(cx), "Lorem @file ");
1991 assert!(editor.has_visible_completions_menu());
1992 });
1993
1994 cx.simulate_input("one");
1995
1996 editor.update(&mut cx, |editor, cx| {
1997 assert_eq!(editor.text(cx), "Lorem @file one");
1998 assert!(editor.has_visible_completions_menu());
1999 assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
2000 });
2001
2002 editor.update_in(&mut cx, |editor, window, cx| {
2003 assert!(editor.has_visible_completions_menu());
2004 editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2005 });
2006
2007 let url_one = uri!("file:///dir/a/one.txt");
2008 editor.update(&mut cx, |editor, cx| {
2009 let text = editor.text(cx);
2010 assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
2011 assert!(!editor.has_visible_completions_menu());
2012 assert_eq!(fold_ranges(editor, cx).len(), 1);
2013 });
2014
2015 let contents = message_editor
2016 .update_in(&mut cx, |message_editor, window, cx| {
2017 message_editor
2018 .mention_set()
2019 .contents(&project, None, window, cx)
2020 })
2021 .await
2022 .unwrap()
2023 .into_values()
2024 .collect::<Vec<_>>();
2025
2026 pretty_assertions::assert_eq!(
2027 contents,
2028 [Mention::Text {
2029 content: "1".into(),
2030 uri: url_one.parse().unwrap()
2031 }]
2032 );
2033
2034 cx.simulate_input(" ");
2035
2036 editor.update(&mut cx, |editor, cx| {
2037 let text = editor.text(cx);
2038 assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
2039 assert!(!editor.has_visible_completions_menu());
2040 assert_eq!(fold_ranges(editor, cx).len(), 1);
2041 });
2042
2043 cx.simulate_input("Ipsum ");
2044
2045 editor.update(&mut cx, |editor, cx| {
2046 let text = editor.text(cx);
2047 assert_eq!(text, format!("Lorem [@one.txt]({url_one}) Ipsum "),);
2048 assert!(!editor.has_visible_completions_menu());
2049 assert_eq!(fold_ranges(editor, cx).len(), 1);
2050 });
2051
2052 cx.simulate_input("@file ");
2053
2054 editor.update(&mut cx, |editor, cx| {
2055 let text = editor.text(cx);
2056 assert_eq!(text, format!("Lorem [@one.txt]({url_one}) Ipsum @file "),);
2057 assert!(editor.has_visible_completions_menu());
2058 assert_eq!(fold_ranges(editor, cx).len(), 1);
2059 });
2060
2061 editor.update_in(&mut cx, |editor, window, cx| {
2062 editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2063 });
2064
2065 cx.run_until_parked();
2066
2067 let contents = message_editor
2068 .update_in(&mut cx, |message_editor, window, cx| {
2069 message_editor
2070 .mention_set()
2071 .contents(&project, None, window, cx)
2072 })
2073 .await
2074 .unwrap()
2075 .into_values()
2076 .collect::<Vec<_>>();
2077
2078 assert_eq!(contents.len(), 2);
2079 let url_eight = uri!("file:///dir/b/eight.txt");
2080 pretty_assertions::assert_eq!(
2081 contents[1],
2082 Mention::Text {
2083 content: "8".to_string(),
2084 uri: url_eight.parse().unwrap(),
2085 }
2086 );
2087
2088 editor.update(&mut cx, |editor, cx| {
2089 assert_eq!(
2090 editor.text(cx),
2091 format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) ")
2092 );
2093 assert!(!editor.has_visible_completions_menu());
2094 assert_eq!(fold_ranges(editor, cx).len(), 2);
2095 });
2096
2097 let plain_text_language = Arc::new(language::Language::new(
2098 language::LanguageConfig {
2099 name: "Plain Text".into(),
2100 matcher: language::LanguageMatcher {
2101 path_suffixes: vec!["txt".to_string()],
2102 ..Default::default()
2103 },
2104 ..Default::default()
2105 },
2106 None,
2107 ));
2108
2109 // Register the language and fake LSP
2110 let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
2111 language_registry.add(plain_text_language);
2112
2113 let mut fake_language_servers = language_registry.register_fake_lsp(
2114 "Plain Text",
2115 language::FakeLspAdapter {
2116 capabilities: lsp::ServerCapabilities {
2117 workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
2118 ..Default::default()
2119 },
2120 ..Default::default()
2121 },
2122 );
2123
2124 // Open the buffer to trigger LSP initialization
2125 let buffer = project
2126 .update(&mut cx, |project, cx| {
2127 project.open_local_buffer(path!("/dir/a/one.txt"), cx)
2128 })
2129 .await
2130 .unwrap();
2131
2132 // Register the buffer with language servers
2133 let _handle = project.update(&mut cx, |project, cx| {
2134 project.register_buffer_with_language_servers(&buffer, cx)
2135 });
2136
2137 cx.run_until_parked();
2138
2139 let fake_language_server = fake_language_servers.next().await.unwrap();
2140 fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
2141 move |_, _| async move {
2142 Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
2143 #[allow(deprecated)]
2144 lsp::SymbolInformation {
2145 name: "MySymbol".into(),
2146 location: lsp::Location {
2147 uri: lsp::Url::from_file_path(path!("/dir/a/one.txt")).unwrap(),
2148 range: lsp::Range::new(
2149 lsp::Position::new(0, 0),
2150 lsp::Position::new(0, 1),
2151 ),
2152 },
2153 kind: lsp::SymbolKind::CONSTANT,
2154 tags: None,
2155 container_name: None,
2156 deprecated: None,
2157 },
2158 ])))
2159 },
2160 );
2161
2162 cx.simulate_input("@symbol ");
2163
2164 editor.update(&mut cx, |editor, cx| {
2165 assert_eq!(
2166 editor.text(cx),
2167 format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) @symbol ")
2168 );
2169 assert!(editor.has_visible_completions_menu());
2170 assert_eq!(current_completion_labels(editor), &["MySymbol"]);
2171 });
2172
2173 editor.update_in(&mut cx, |editor, window, cx| {
2174 editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
2175 });
2176
2177 let contents = message_editor
2178 .update_in(&mut cx, |message_editor, window, cx| {
2179 message_editor
2180 .mention_set()
2181 .contents(&project, None, window, cx)
2182 })
2183 .await
2184 .unwrap()
2185 .into_values()
2186 .collect::<Vec<_>>();
2187
2188 assert_eq!(contents.len(), 3);
2189 pretty_assertions::assert_eq!(
2190 contents[2],
2191 Mention::Text {
2192 content: "1".into(),
2193 uri: format!("{url_one}?symbol=MySymbol#L1:1").parse().unwrap(),
2194 }
2195 );
2196
2197 cx.run_until_parked();
2198
2199 editor.read_with(&cx, |editor, cx| {
2200 assert_eq!(
2201 editor.text(cx),
2202 format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
2203 );
2204 });
2205 }
2206
2207 fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
2208 let snapshot = editor.buffer().read(cx).snapshot(cx);
2209 editor.display_map.update(cx, |display_map, cx| {
2210 display_map
2211 .snapshot(cx)
2212 .folds_in_range(0..snapshot.len())
2213 .map(|fold| fold.range.to_point(&snapshot))
2214 .collect()
2215 })
2216 }
2217
2218 fn current_completion_labels(editor: &Editor) -> Vec<String> {
2219 let completions = editor.current_completions().expect("Missing completions");
2220 completions
2221 .into_iter()
2222 .map(|completion| completion.label.text.to_string())
2223 .collect::<Vec<_>>()
2224 }
2225}