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