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