message_editor.rs

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