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