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