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