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