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