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