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