completion_provider.rs

   1use std::ffi::OsStr;
   2use std::ops::Range;
   3use std::path::Path;
   4use std::sync::Arc;
   5use std::sync::atomic::AtomicBool;
   6
   7use acp_thread::MentionUri;
   8use anyhow::{Context as _, Result, anyhow};
   9use collections::HashMap;
  10use editor::display_map::CreaseId;
  11use editor::{CompletionProvider, Editor, ExcerptId};
  12use futures::future::{Shared, try_join_all};
  13use fuzzy::{StringMatch, StringMatchCandidate};
  14use gpui::{App, Entity, ImageFormat, Img, Task, WeakEntity};
  15use http_client::HttpClientWithUrl;
  16use language::{Buffer, CodeLabel, HighlightId};
  17use language_model::LanguageModelImage;
  18use lsp::CompletionContext;
  19use project::{
  20    Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId,
  21};
  22use prompt_store::PromptStore;
  23use rope::Point;
  24use text::{Anchor, ToPoint as _};
  25use ui::prelude::*;
  26use url::Url;
  27use workspace::Workspace;
  28
  29use agent::thread_store::{TextThreadStore, ThreadStore};
  30
  31use crate::acp::message_editor::MessageEditor;
  32use crate::context_picker::file_context_picker::{FileMatch, search_files};
  33use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules};
  34use crate::context_picker::symbol_context_picker::SymbolMatch;
  35use crate::context_picker::symbol_context_picker::search_symbols;
  36use crate::context_picker::thread_context_picker::{
  37    ThreadContextEntry, ThreadMatch, search_threads,
  38};
  39use crate::context_picker::{
  40    ContextPickerAction, ContextPickerEntry, ContextPickerMode, RecentEntry,
  41    available_context_picker_entries, recent_context_picker_entries, selection_ranges,
  42};
  43
  44#[derive(Clone, Debug, Eq, PartialEq)]
  45pub struct MentionImage {
  46    pub abs_path: Option<Arc<Path>>,
  47    pub data: SharedString,
  48    pub format: ImageFormat,
  49}
  50
  51#[derive(Default)]
  52pub struct MentionSet {
  53    pub(crate) uri_by_crease_id: HashMap<CreaseId, MentionUri>,
  54    fetch_results: HashMap<Url, Shared<Task<Result<String, String>>>>,
  55    images: HashMap<CreaseId, Shared<Task<Result<MentionImage, String>>>>,
  56}
  57
  58impl MentionSet {
  59    pub fn insert_uri(&mut self, crease_id: CreaseId, uri: MentionUri) {
  60        self.uri_by_crease_id.insert(crease_id, uri);
  61    }
  62
  63    pub fn add_fetch_result(&mut self, url: Url, content: Shared<Task<Result<String, String>>>) {
  64        self.fetch_results.insert(url, content);
  65    }
  66
  67    pub fn insert_image(
  68        &mut self,
  69        crease_id: CreaseId,
  70        task: Shared<Task<Result<MentionImage, String>>>,
  71    ) {
  72        self.images.insert(crease_id, task);
  73    }
  74
  75    pub fn drain(&mut self) -> impl Iterator<Item = CreaseId> {
  76        self.fetch_results.clear();
  77        self.uri_by_crease_id
  78            .drain()
  79            .map(|(id, _)| id)
  80            .chain(self.images.drain().map(|(id, _)| id))
  81    }
  82
  83    pub fn clear(&mut self) {
  84        self.fetch_results.clear();
  85        self.uri_by_crease_id.clear();
  86    }
  87
  88    pub fn contents(
  89        &self,
  90        project: Entity<Project>,
  91        thread_store: Entity<ThreadStore>,
  92        text_thread_store: Entity<TextThreadStore>,
  93        window: &mut Window,
  94        cx: &mut App,
  95    ) -> Task<Result<HashMap<CreaseId, Mention>>> {
  96        let mut contents = self
  97            .uri_by_crease_id
  98            .iter()
  99            .map(|(&crease_id, uri)| {
 100                match uri {
 101                    MentionUri::File { abs_path, .. } => {
 102                        // TODO directories
 103                        let uri = uri.clone();
 104                        let abs_path = abs_path.to_path_buf();
 105                        let extension = abs_path.extension().and_then(OsStr::to_str).unwrap_or("");
 106
 107                        if Img::extensions().contains(&extension) && !extension.contains("svg") {
 108                            let open_image_task = project.update(cx, |project, cx| {
 109                                let path = project
 110                                    .find_project_path(&abs_path, cx)
 111                                    .context("Failed to find project path")?;
 112                                anyhow::Ok(project.open_image(path, cx))
 113                            });
 114
 115                            cx.spawn(async move |cx| {
 116                                let image_item = open_image_task?.await?;
 117                                let (data, format) = image_item.update(cx, |image_item, cx| {
 118                                    let format = image_item.image.format;
 119                                    (
 120                                        LanguageModelImage::from_image(
 121                                            image_item.image.clone(),
 122                                            cx,
 123                                        ),
 124                                        format,
 125                                    )
 126                                })?;
 127                                let data = cx.spawn(async move |_| {
 128                                    if let Some(data) = data.await {
 129                                        Ok(data.source)
 130                                    } else {
 131                                        anyhow::bail!("Failed to convert image")
 132                                    }
 133                                });
 134
 135                                anyhow::Ok((
 136                                    crease_id,
 137                                    Mention::Image(MentionImage {
 138                                        abs_path: Some(abs_path.as_path().into()),
 139                                        data: data.await?,
 140                                        format,
 141                                    }),
 142                                ))
 143                            })
 144                        } else {
 145                            let buffer_task = project.update(cx, |project, cx| {
 146                                let path = project
 147                                    .find_project_path(abs_path, cx)
 148                                    .context("Failed to find project path")?;
 149                                anyhow::Ok(project.open_buffer(path, cx))
 150                            });
 151                            cx.spawn(async move |cx| {
 152                                let buffer = buffer_task?.await?;
 153                                let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
 154
 155                                anyhow::Ok((crease_id, Mention::Text { uri, content }))
 156                            })
 157                        }
 158                    }
 159                    MentionUri::Symbol {
 160                        path, line_range, ..
 161                    }
 162                    | MentionUri::Selection {
 163                        path, line_range, ..
 164                    } => {
 165                        let uri = uri.clone();
 166                        let path_buf = path.clone();
 167                        let line_range = line_range.clone();
 168
 169                        let buffer_task = project.update(cx, |project, cx| {
 170                            let path = project
 171                                .find_project_path(&path_buf, cx)
 172                                .context("Failed to find project path")?;
 173                            anyhow::Ok(project.open_buffer(path, cx))
 174                        });
 175
 176                        cx.spawn(async move |cx| {
 177                            let buffer = buffer_task?.await?;
 178                            let content = buffer.read_with(cx, |buffer, _cx| {
 179                                buffer
 180                                    .text_for_range(
 181                                        Point::new(line_range.start, 0)
 182                                            ..Point::new(
 183                                                line_range.end,
 184                                                buffer.line_len(line_range.end),
 185                                            ),
 186                                    )
 187                                    .collect()
 188                            })?;
 189
 190                            anyhow::Ok((crease_id, Mention::Text { uri, content }))
 191                        })
 192                    }
 193                    MentionUri::Thread { id: thread_id, .. } => {
 194                        let open_task = thread_store.update(cx, |thread_store, cx| {
 195                            thread_store.open_thread(&thread_id, window, cx)
 196                        });
 197
 198                        let uri = uri.clone();
 199                        cx.spawn(async move |cx| {
 200                            let thread = open_task.await?;
 201                            let content = thread.read_with(cx, |thread, _cx| {
 202                                thread.latest_detailed_summary_or_text().to_string()
 203                            })?;
 204
 205                            anyhow::Ok((crease_id, Mention::Text { uri, content }))
 206                        })
 207                    }
 208                    MentionUri::TextThread { path, .. } => {
 209                        let context = text_thread_store.update(cx, |text_thread_store, cx| {
 210                            text_thread_store.open_local_context(path.as_path().into(), cx)
 211                        });
 212                        let uri = uri.clone();
 213                        cx.spawn(async move |cx| {
 214                            let context = context.await?;
 215                            let xml = context.update(cx, |context, cx| context.to_xml(cx))?;
 216                            anyhow::Ok((crease_id, Mention::Text { uri, content: xml }))
 217                        })
 218                    }
 219                    MentionUri::Rule { id: prompt_id, .. } => {
 220                        let Some(prompt_store) = thread_store.read(cx).prompt_store().clone()
 221                        else {
 222                            return Task::ready(Err(anyhow!("missing prompt store")));
 223                        };
 224                        let text_task = prompt_store.read(cx).load(*prompt_id, cx);
 225                        let uri = uri.clone();
 226                        cx.spawn(async move |_| {
 227                            // TODO: report load errors instead of just logging
 228                            let text = text_task.await?;
 229                            anyhow::Ok((crease_id, Mention::Text { uri, content: text }))
 230                        })
 231                    }
 232                    MentionUri::Fetch { url } => {
 233                        let Some(content) = self.fetch_results.get(&url).cloned() else {
 234                            return Task::ready(Err(anyhow!("missing fetch result")));
 235                        };
 236                        let uri = uri.clone();
 237                        cx.spawn(async move |_| {
 238                            Ok((
 239                                crease_id,
 240                                Mention::Text {
 241                                    uri,
 242                                    content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?,
 243                                },
 244                            ))
 245                        })
 246                    }
 247                }
 248            })
 249            .collect::<Vec<_>>();
 250
 251        contents.extend(self.images.iter().map(|(crease_id, image)| {
 252            let crease_id = *crease_id;
 253            let image = image.clone();
 254            cx.spawn(async move |_| {
 255                Ok((
 256                    crease_id,
 257                    Mention::Image(image.await.map_err(|e| anyhow::anyhow!("{e}"))?),
 258                ))
 259            })
 260        }));
 261
 262        cx.spawn(async move |_cx| {
 263            let contents = try_join_all(contents).await?.into_iter().collect();
 264            anyhow::Ok(contents)
 265        })
 266    }
 267}
 268
 269#[derive(Debug, Eq, PartialEq)]
 270pub enum Mention {
 271    Text { uri: MentionUri, content: String },
 272    Image(MentionImage),
 273}
 274
 275pub(crate) enum Match {
 276    File(FileMatch),
 277    Symbol(SymbolMatch),
 278    Thread(ThreadMatch),
 279    Fetch(SharedString),
 280    Rules(RulesContextEntry),
 281    Entry(EntryMatch),
 282}
 283
 284pub struct EntryMatch {
 285    mat: Option<StringMatch>,
 286    entry: ContextPickerEntry,
 287}
 288
 289impl Match {
 290    pub fn score(&self) -> f64 {
 291        match self {
 292            Match::File(file) => file.mat.score,
 293            Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
 294            Match::Thread(_) => 1.,
 295            Match::Symbol(_) => 1.,
 296            Match::Rules(_) => 1.,
 297            Match::Fetch(_) => 1.,
 298        }
 299    }
 300}
 301
 302fn search(
 303    mode: Option<ContextPickerMode>,
 304    query: String,
 305    cancellation_flag: Arc<AtomicBool>,
 306    recent_entries: Vec<RecentEntry>,
 307    prompt_store: Option<Entity<PromptStore>>,
 308    thread_store: WeakEntity<ThreadStore>,
 309    text_thread_context_store: WeakEntity<assistant_context::ContextStore>,
 310    workspace: Entity<Workspace>,
 311    cx: &mut App,
 312) -> Task<Vec<Match>> {
 313    match mode {
 314        Some(ContextPickerMode::File) => {
 315            let search_files_task =
 316                search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
 317            cx.background_spawn(async move {
 318                search_files_task
 319                    .await
 320                    .into_iter()
 321                    .map(Match::File)
 322                    .collect()
 323            })
 324        }
 325
 326        Some(ContextPickerMode::Symbol) => {
 327            let search_symbols_task =
 328                search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx);
 329            cx.background_spawn(async move {
 330                search_symbols_task
 331                    .await
 332                    .into_iter()
 333                    .map(Match::Symbol)
 334                    .collect()
 335            })
 336        }
 337
 338        Some(ContextPickerMode::Thread) => {
 339            if let Some((thread_store, context_store)) = thread_store
 340                .upgrade()
 341                .zip(text_thread_context_store.upgrade())
 342            {
 343                let search_threads_task = search_threads(
 344                    query.clone(),
 345                    cancellation_flag.clone(),
 346                    thread_store,
 347                    context_store,
 348                    cx,
 349                );
 350                cx.background_spawn(async move {
 351                    search_threads_task
 352                        .await
 353                        .into_iter()
 354                        .map(Match::Thread)
 355                        .collect()
 356                })
 357            } else {
 358                Task::ready(Vec::new())
 359            }
 360        }
 361
 362        Some(ContextPickerMode::Fetch) => {
 363            if !query.is_empty() {
 364                Task::ready(vec![Match::Fetch(query.into())])
 365            } else {
 366                Task::ready(Vec::new())
 367            }
 368        }
 369
 370        Some(ContextPickerMode::Rules) => {
 371            if let Some(prompt_store) = prompt_store.as_ref() {
 372                let search_rules_task =
 373                    search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx);
 374                cx.background_spawn(async move {
 375                    search_rules_task
 376                        .await
 377                        .into_iter()
 378                        .map(Match::Rules)
 379                        .collect::<Vec<_>>()
 380                })
 381            } else {
 382                Task::ready(Vec::new())
 383            }
 384        }
 385
 386        None => {
 387            if query.is_empty() {
 388                let mut matches = recent_entries
 389                    .into_iter()
 390                    .map(|entry| match entry {
 391                        RecentEntry::File {
 392                            project_path,
 393                            path_prefix,
 394                        } => Match::File(FileMatch {
 395                            mat: fuzzy::PathMatch {
 396                                score: 1.,
 397                                positions: Vec::new(),
 398                                worktree_id: project_path.worktree_id.to_usize(),
 399                                path: project_path.path,
 400                                path_prefix,
 401                                is_dir: false,
 402                                distance_to_relative_ancestor: 0,
 403                            },
 404                            is_recent: true,
 405                        }),
 406                        RecentEntry::Thread(thread_context_entry) => Match::Thread(ThreadMatch {
 407                            thread: thread_context_entry,
 408                            is_recent: true,
 409                        }),
 410                    })
 411                    .collect::<Vec<_>>();
 412
 413                matches.extend(
 414                    available_context_picker_entries(
 415                        &prompt_store,
 416                        &Some(thread_store.clone()),
 417                        &workspace,
 418                        cx,
 419                    )
 420                    .into_iter()
 421                    .map(|mode| {
 422                        Match::Entry(EntryMatch {
 423                            entry: mode,
 424                            mat: None,
 425                        })
 426                    }),
 427                );
 428
 429                Task::ready(matches)
 430            } else {
 431                let executor = cx.background_executor().clone();
 432
 433                let search_files_task =
 434                    search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
 435
 436                let entries = available_context_picker_entries(
 437                    &prompt_store,
 438                    &Some(thread_store.clone()),
 439                    &workspace,
 440                    cx,
 441                );
 442                let entry_candidates = entries
 443                    .iter()
 444                    .enumerate()
 445                    .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword()))
 446                    .collect::<Vec<_>>();
 447
 448                cx.background_spawn(async move {
 449                    let mut matches = search_files_task
 450                        .await
 451                        .into_iter()
 452                        .map(Match::File)
 453                        .collect::<Vec<_>>();
 454
 455                    let entry_matches = fuzzy::match_strings(
 456                        &entry_candidates,
 457                        &query,
 458                        false,
 459                        true,
 460                        100,
 461                        &Arc::new(AtomicBool::default()),
 462                        executor,
 463                    )
 464                    .await;
 465
 466                    matches.extend(entry_matches.into_iter().map(|mat| {
 467                        Match::Entry(EntryMatch {
 468                            entry: entries[mat.candidate_id],
 469                            mat: Some(mat),
 470                        })
 471                    }));
 472
 473                    matches.sort_by(|a, b| {
 474                        b.score()
 475                            .partial_cmp(&a.score())
 476                            .unwrap_or(std::cmp::Ordering::Equal)
 477                    });
 478
 479                    matches
 480                })
 481            }
 482        }
 483    }
 484}
 485
 486pub struct ContextPickerCompletionProvider {
 487    workspace: WeakEntity<Workspace>,
 488    thread_store: WeakEntity<ThreadStore>,
 489    text_thread_store: WeakEntity<TextThreadStore>,
 490    message_editor: WeakEntity<MessageEditor>,
 491}
 492
 493impl ContextPickerCompletionProvider {
 494    pub fn new(
 495        workspace: WeakEntity<Workspace>,
 496        thread_store: WeakEntity<ThreadStore>,
 497        text_thread_store: WeakEntity<TextThreadStore>,
 498        message_editor: WeakEntity<MessageEditor>,
 499    ) -> Self {
 500        Self {
 501            workspace,
 502            thread_store,
 503            text_thread_store,
 504            message_editor,
 505        }
 506    }
 507
 508    fn completion_for_entry(
 509        entry: ContextPickerEntry,
 510        source_range: Range<Anchor>,
 511        message_editor: WeakEntity<MessageEditor>,
 512        workspace: &Entity<Workspace>,
 513        cx: &mut App,
 514    ) -> Option<Completion> {
 515        match entry {
 516            ContextPickerEntry::Mode(mode) => Some(Completion {
 517                replace_range: source_range.clone(),
 518                new_text: format!("@{} ", mode.keyword()),
 519                label: CodeLabel::plain(mode.label().to_string(), None),
 520                icon_path: Some(mode.icon().path().into()),
 521                documentation: None,
 522                source: project::CompletionSource::Custom,
 523                insert_text_mode: None,
 524                // This ensures that when a user accepts this completion, the
 525                // completion menu will still be shown after "@category " is
 526                // inserted
 527                confirm: Some(Arc::new(|_, _, _| true)),
 528            }),
 529            ContextPickerEntry::Action(action) => {
 530                let (new_text, on_action) = match action {
 531                    ContextPickerAction::AddSelections => {
 532                        const PLACEHOLDER: &str = "selection ";
 533                        let selections = selection_ranges(workspace, cx)
 534                            .into_iter()
 535                            .enumerate()
 536                            .map(|(ix, (buffer, range))| {
 537                                (
 538                                    buffer,
 539                                    range,
 540                                    (PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1),
 541                                )
 542                            })
 543                            .collect::<Vec<_>>();
 544
 545                        let new_text: String = PLACEHOLDER.repeat(selections.len());
 546
 547                        let callback = Arc::new({
 548                            let source_range = source_range.clone();
 549                            move |_, window: &mut Window, cx: &mut App| {
 550                                let selections = selections.clone();
 551                                let message_editor = message_editor.clone();
 552                                let source_range = source_range.clone();
 553                                window.defer(cx, move |window, cx| {
 554                                    message_editor
 555                                        .update(cx, |message_editor, cx| {
 556                                            message_editor.confirm_mention_for_selection(
 557                                                source_range,
 558                                                selections,
 559                                                window,
 560                                                cx,
 561                                            )
 562                                        })
 563                                        .ok();
 564                                });
 565                                false
 566                            }
 567                        });
 568
 569                        (new_text, callback)
 570                    }
 571                };
 572
 573                Some(Completion {
 574                    replace_range: source_range.clone(),
 575                    new_text,
 576                    label: CodeLabel::plain(action.label().to_string(), None),
 577                    icon_path: Some(action.icon().path().into()),
 578                    documentation: None,
 579                    source: project::CompletionSource::Custom,
 580                    insert_text_mode: None,
 581                    // This ensures that when a user accepts this completion, the
 582                    // completion menu will still be shown after "@category " is
 583                    // inserted
 584                    confirm: Some(on_action),
 585                })
 586            }
 587        }
 588    }
 589
 590    fn completion_for_thread(
 591        thread_entry: ThreadContextEntry,
 592        source_range: Range<Anchor>,
 593        recent: bool,
 594        editor: WeakEntity<MessageEditor>,
 595        cx: &mut App,
 596    ) -> Completion {
 597        let uri = match &thread_entry {
 598            ThreadContextEntry::Thread { id, title } => MentionUri::Thread {
 599                id: id.clone(),
 600                name: title.to_string(),
 601            },
 602            ThreadContextEntry::Context { path, title } => MentionUri::TextThread {
 603                path: path.to_path_buf(),
 604                name: title.to_string(),
 605            },
 606        };
 607
 608        let icon_for_completion = if recent {
 609            IconName::HistoryRerun.path().into()
 610        } else {
 611            uri.icon_path(cx)
 612        };
 613
 614        let new_text = format!("{} ", uri.as_link());
 615
 616        let new_text_len = new_text.len();
 617        Completion {
 618            replace_range: source_range.clone(),
 619            new_text,
 620            label: CodeLabel::plain(thread_entry.title().to_string(), None),
 621            documentation: None,
 622            insert_text_mode: None,
 623            source: project::CompletionSource::Custom,
 624            icon_path: Some(icon_for_completion.clone()),
 625            confirm: Some(confirm_completion_callback(
 626                thread_entry.title().clone(),
 627                source_range.start,
 628                new_text_len - 1,
 629                editor,
 630                uri,
 631            )),
 632        }
 633    }
 634
 635    fn completion_for_rules(
 636        rule: RulesContextEntry,
 637        source_range: Range<Anchor>,
 638        editor: WeakEntity<MessageEditor>,
 639        cx: &mut App,
 640    ) -> Completion {
 641        let uri = MentionUri::Rule {
 642            id: rule.prompt_id.into(),
 643            name: rule.title.to_string(),
 644        };
 645        let new_text = format!("{} ", uri.as_link());
 646        let new_text_len = new_text.len();
 647        let icon_path = uri.icon_path(cx);
 648        Completion {
 649            replace_range: source_range.clone(),
 650            new_text,
 651            label: CodeLabel::plain(rule.title.to_string(), None),
 652            documentation: None,
 653            insert_text_mode: None,
 654            source: project::CompletionSource::Custom,
 655            icon_path: Some(icon_path.clone()),
 656            confirm: Some(confirm_completion_callback(
 657                rule.title.clone(),
 658                source_range.start,
 659                new_text_len - 1,
 660                editor,
 661                uri,
 662            )),
 663        }
 664    }
 665
 666    pub(crate) fn completion_for_path(
 667        project_path: ProjectPath,
 668        path_prefix: &str,
 669        is_recent: bool,
 670        is_directory: bool,
 671        source_range: Range<Anchor>,
 672        message_editor: WeakEntity<MessageEditor>,
 673        project: Entity<Project>,
 674        cx: &mut App,
 675    ) -> Option<Completion> {
 676        let (file_name, directory) =
 677            crate::context_picker::file_context_picker::extract_file_name_and_directory(
 678                &project_path.path,
 679                path_prefix,
 680            );
 681
 682        let label =
 683            build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
 684
 685        let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
 686
 687        let file_uri = MentionUri::File {
 688            abs_path,
 689            is_directory,
 690        };
 691
 692        let crease_icon_path = file_uri.icon_path(cx);
 693        let completion_icon_path = if is_recent {
 694            IconName::HistoryRerun.path().into()
 695        } else {
 696            crease_icon_path.clone()
 697        };
 698
 699        let new_text = format!("{} ", file_uri.as_link());
 700        let new_text_len = new_text.len();
 701        Some(Completion {
 702            replace_range: source_range.clone(),
 703            new_text,
 704            label,
 705            documentation: None,
 706            source: project::CompletionSource::Custom,
 707            icon_path: Some(completion_icon_path),
 708            insert_text_mode: None,
 709            confirm: Some(confirm_completion_callback(
 710                file_name,
 711                source_range.start,
 712                new_text_len - 1,
 713                message_editor,
 714                file_uri,
 715            )),
 716        })
 717    }
 718
 719    fn completion_for_symbol(
 720        symbol: Symbol,
 721        source_range: Range<Anchor>,
 722        message_editor: WeakEntity<MessageEditor>,
 723        workspace: Entity<Workspace>,
 724        cx: &mut App,
 725    ) -> Option<Completion> {
 726        let project = workspace.read(cx).project().clone();
 727
 728        let label = CodeLabel::plain(symbol.name.clone(), None);
 729
 730        let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?;
 731        let uri = MentionUri::Symbol {
 732            path: abs_path,
 733            name: symbol.name.clone(),
 734            line_range: symbol.range.start.0.row..symbol.range.end.0.row,
 735        };
 736        let new_text = format!("{} ", uri.as_link());
 737        let new_text_len = new_text.len();
 738        let icon_path = uri.icon_path(cx);
 739        Some(Completion {
 740            replace_range: source_range.clone(),
 741            new_text,
 742            label,
 743            documentation: None,
 744            source: project::CompletionSource::Custom,
 745            icon_path: Some(icon_path.clone()),
 746            insert_text_mode: None,
 747            confirm: Some(confirm_completion_callback(
 748                symbol.name.clone().into(),
 749                source_range.start,
 750                new_text_len - 1,
 751                message_editor,
 752                uri,
 753            )),
 754        })
 755    }
 756
 757    fn completion_for_fetch(
 758        source_range: Range<Anchor>,
 759        url_to_fetch: SharedString,
 760        message_editor: WeakEntity<MessageEditor>,
 761        http_client: Arc<HttpClientWithUrl>,
 762        cx: &mut App,
 763    ) -> Option<Completion> {
 764        let new_text = format!("@fetch {} ", url_to_fetch.clone());
 765        let url_to_fetch = url::Url::parse(url_to_fetch.as_ref())
 766            .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}")))
 767            .ok()?;
 768        let mention_uri = MentionUri::Fetch {
 769            url: url_to_fetch.clone(),
 770        };
 771        let icon_path = mention_uri.icon_path(cx);
 772        Some(Completion {
 773            replace_range: source_range.clone(),
 774            new_text: new_text.clone(),
 775            label: CodeLabel::plain(url_to_fetch.to_string(), None),
 776            documentation: None,
 777            source: project::CompletionSource::Custom,
 778            icon_path: Some(icon_path.clone()),
 779            insert_text_mode: None,
 780            confirm: Some({
 781                Arc::new(move |_, window, cx| {
 782                    let url_to_fetch = url_to_fetch.clone();
 783                    let source_range = source_range.clone();
 784                    let message_editor = message_editor.clone();
 785                    let new_text = new_text.clone();
 786                    let http_client = http_client.clone();
 787                    window.defer(cx, move |window, cx| {
 788                        message_editor
 789                            .update(cx, |message_editor, cx| {
 790                                message_editor.confirm_mention_for_fetch(
 791                                    new_text,
 792                                    source_range,
 793                                    url_to_fetch,
 794                                    http_client,
 795                                    window,
 796                                    cx,
 797                                )
 798                            })
 799                            .ok();
 800                    });
 801                    false
 802                })
 803            }),
 804        })
 805    }
 806}
 807
 808fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
 809    let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
 810    let mut label = CodeLabel::default();
 811
 812    label.push_str(&file_name, None);
 813    label.push_str(" ", None);
 814
 815    if let Some(directory) = directory {
 816        label.push_str(&directory, comment_id);
 817    }
 818
 819    label.filter_range = 0..label.text().len();
 820
 821    label
 822}
 823
 824impl CompletionProvider for ContextPickerCompletionProvider {
 825    fn completions(
 826        &self,
 827        _excerpt_id: ExcerptId,
 828        buffer: &Entity<Buffer>,
 829        buffer_position: Anchor,
 830        _trigger: CompletionContext,
 831        _window: &mut Window,
 832        cx: &mut Context<Editor>,
 833    ) -> Task<Result<Vec<CompletionResponse>>> {
 834        let state = buffer.update(cx, |buffer, _cx| {
 835            let position = buffer_position.to_point(buffer);
 836            let line_start = Point::new(position.row, 0);
 837            let offset_to_line = buffer.point_to_offset(line_start);
 838            let mut lines = buffer.text_for_range(line_start..position).lines();
 839            let line = lines.next()?;
 840            MentionCompletion::try_parse(line, offset_to_line)
 841        });
 842        let Some(state) = state else {
 843            return Task::ready(Ok(Vec::new()));
 844        };
 845
 846        let Some(workspace) = self.workspace.upgrade() else {
 847            return Task::ready(Ok(Vec::new()));
 848        };
 849
 850        let project = workspace.read(cx).project().clone();
 851        let http_client = workspace.read(cx).client().http_client();
 852        let snapshot = buffer.read(cx).snapshot();
 853        let source_range = snapshot.anchor_before(state.source_range.start)
 854            ..snapshot.anchor_after(state.source_range.end);
 855
 856        let thread_store = self.thread_store.clone();
 857        let text_thread_store = self.text_thread_store.clone();
 858        let editor = self.message_editor.clone();
 859        let Ok((exclude_paths, exclude_threads)) =
 860            self.message_editor.update(cx, |message_editor, cx| {
 861                message_editor.mentioned_path_and_threads(cx)
 862            })
 863        else {
 864            return Task::ready(Ok(Vec::new()));
 865        };
 866
 867        let MentionCompletion { mode, argument, .. } = state;
 868        let query = argument.unwrap_or_else(|| "".to_string());
 869
 870        let recent_entries = recent_context_picker_entries(
 871            Some(thread_store.clone()),
 872            Some(text_thread_store.clone()),
 873            workspace.clone(),
 874            &exclude_paths,
 875            &exclude_threads,
 876            cx,
 877        );
 878
 879        let prompt_store = thread_store
 880            .read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone())
 881            .ok()
 882            .flatten();
 883
 884        let search_task = search(
 885            mode,
 886            query,
 887            Arc::<AtomicBool>::default(),
 888            recent_entries,
 889            prompt_store,
 890            thread_store.clone(),
 891            text_thread_store.clone(),
 892            workspace.clone(),
 893            cx,
 894        );
 895
 896        cx.spawn(async move |_, cx| {
 897            let matches = search_task.await;
 898
 899            let completions = cx.update(|cx| {
 900                matches
 901                    .into_iter()
 902                    .filter_map(|mat| match mat {
 903                        Match::File(FileMatch { mat, is_recent }) => {
 904                            let project_path = ProjectPath {
 905                                worktree_id: WorktreeId::from_usize(mat.worktree_id),
 906                                path: mat.path.clone(),
 907                            };
 908
 909                            Self::completion_for_path(
 910                                project_path,
 911                                &mat.path_prefix,
 912                                is_recent,
 913                                mat.is_dir,
 914                                source_range.clone(),
 915                                editor.clone(),
 916                                project.clone(),
 917                                cx,
 918                            )
 919                        }
 920
 921                        Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
 922                            symbol,
 923                            source_range.clone(),
 924                            editor.clone(),
 925                            workspace.clone(),
 926                            cx,
 927                        ),
 928
 929                        Match::Thread(ThreadMatch {
 930                            thread, is_recent, ..
 931                        }) => Some(Self::completion_for_thread(
 932                            thread,
 933                            source_range.clone(),
 934                            is_recent,
 935                            editor.clone(),
 936                            cx,
 937                        )),
 938
 939                        Match::Rules(user_rules) => Some(Self::completion_for_rules(
 940                            user_rules,
 941                            source_range.clone(),
 942                            editor.clone(),
 943                            cx,
 944                        )),
 945
 946                        Match::Fetch(url) => Self::completion_for_fetch(
 947                            source_range.clone(),
 948                            url,
 949                            editor.clone(),
 950                            http_client.clone(),
 951                            cx,
 952                        ),
 953
 954                        Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
 955                            entry,
 956                            source_range.clone(),
 957                            editor.clone(),
 958                            &workspace,
 959                            cx,
 960                        ),
 961                    })
 962                    .collect()
 963            })?;
 964
 965            Ok(vec![CompletionResponse {
 966                completions,
 967                // Since this does its own filtering (see `filter_completions()` returns false),
 968                // there is no benefit to computing whether this set of completions is incomplete.
 969                is_incomplete: true,
 970            }])
 971        })
 972    }
 973
 974    fn is_completion_trigger(
 975        &self,
 976        buffer: &Entity<language::Buffer>,
 977        position: language::Anchor,
 978        _text: &str,
 979        _trigger_in_words: bool,
 980        _menu_is_open: bool,
 981        cx: &mut Context<Editor>,
 982    ) -> bool {
 983        let buffer = buffer.read(cx);
 984        let position = position.to_point(buffer);
 985        let line_start = Point::new(position.row, 0);
 986        let offset_to_line = buffer.point_to_offset(line_start);
 987        let mut lines = buffer.text_for_range(line_start..position).lines();
 988        if let Some(line) = lines.next() {
 989            MentionCompletion::try_parse(line, offset_to_line)
 990                .map(|completion| {
 991                    completion.source_range.start <= offset_to_line + position.column as usize
 992                        && completion.source_range.end >= offset_to_line + position.column as usize
 993                })
 994                .unwrap_or(false)
 995        } else {
 996            false
 997        }
 998    }
 999
1000    fn sort_completions(&self) -> bool {
1001        false
1002    }
1003
1004    fn filter_completions(&self) -> bool {
1005        false
1006    }
1007}
1008
1009fn confirm_completion_callback(
1010    crease_text: SharedString,
1011    start: Anchor,
1012    content_len: usize,
1013    message_editor: WeakEntity<MessageEditor>,
1014    mention_uri: MentionUri,
1015) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
1016    Arc::new(move |_, window, cx| {
1017        let message_editor = message_editor.clone();
1018        let crease_text = crease_text.clone();
1019        let mention_uri = mention_uri.clone();
1020        window.defer(cx, move |window, cx| {
1021            message_editor
1022                .clone()
1023                .update(cx, |message_editor, cx| {
1024                    message_editor.confirm_completion(
1025                        crease_text,
1026                        start,
1027                        content_len,
1028                        mention_uri,
1029                        window,
1030                        cx,
1031                    )
1032                })
1033                .ok();
1034        });
1035        false
1036    })
1037}
1038
1039#[derive(Debug, Default, PartialEq)]
1040struct MentionCompletion {
1041    source_range: Range<usize>,
1042    mode: Option<ContextPickerMode>,
1043    argument: Option<String>,
1044}
1045
1046impl MentionCompletion {
1047    fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
1048        let last_mention_start = line.rfind('@')?;
1049        if last_mention_start >= line.len() {
1050            return Some(Self::default());
1051        }
1052        if last_mention_start > 0
1053            && line
1054                .chars()
1055                .nth(last_mention_start - 1)
1056                .map_or(false, |c| !c.is_whitespace())
1057        {
1058            return None;
1059        }
1060
1061        let rest_of_line = &line[last_mention_start + 1..];
1062
1063        let mut mode = None;
1064        let mut argument = None;
1065
1066        let mut parts = rest_of_line.split_whitespace();
1067        let mut end = last_mention_start + 1;
1068        if let Some(mode_text) = parts.next() {
1069            end += mode_text.len();
1070
1071            if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok() {
1072                mode = Some(parsed_mode);
1073            } else {
1074                argument = Some(mode_text.to_string());
1075            }
1076            match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
1077                Some(whitespace_count) => {
1078                    if let Some(argument_text) = parts.next() {
1079                        argument = Some(argument_text.to_string());
1080                        end += whitespace_count + argument_text.len();
1081                    }
1082                }
1083                None => {
1084                    // Rest of line is entirely whitespace
1085                    end += rest_of_line.len() - mode_text.len();
1086                }
1087            }
1088        }
1089
1090        Some(Self {
1091            source_range: last_mention_start + offset_to_line..end + offset_to_line,
1092            mode,
1093            argument,
1094        })
1095    }
1096}
1097
1098#[cfg(test)]
1099mod tests {
1100    use super::*;
1101    use editor::{AnchorRangeExt, EditorMode};
1102    use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext};
1103    use project::{Project, ProjectPath};
1104    use serde_json::json;
1105    use settings::SettingsStore;
1106    use smol::stream::StreamExt as _;
1107    use std::{ops::Deref, path::Path};
1108    use util::path;
1109    use workspace::{AppState, Item};
1110
1111    #[test]
1112    fn test_mention_completion_parse() {
1113        assert_eq!(MentionCompletion::try_parse("Lorem Ipsum", 0), None);
1114
1115        assert_eq!(
1116            MentionCompletion::try_parse("Lorem @", 0),
1117            Some(MentionCompletion {
1118                source_range: 6..7,
1119                mode: None,
1120                argument: None,
1121            })
1122        );
1123
1124        assert_eq!(
1125            MentionCompletion::try_parse("Lorem @file", 0),
1126            Some(MentionCompletion {
1127                source_range: 6..11,
1128                mode: Some(ContextPickerMode::File),
1129                argument: None,
1130            })
1131        );
1132
1133        assert_eq!(
1134            MentionCompletion::try_parse("Lorem @file ", 0),
1135            Some(MentionCompletion {
1136                source_range: 6..12,
1137                mode: Some(ContextPickerMode::File),
1138                argument: None,
1139            })
1140        );
1141
1142        assert_eq!(
1143            MentionCompletion::try_parse("Lorem @file main.rs", 0),
1144            Some(MentionCompletion {
1145                source_range: 6..19,
1146                mode: Some(ContextPickerMode::File),
1147                argument: Some("main.rs".to_string()),
1148            })
1149        );
1150
1151        assert_eq!(
1152            MentionCompletion::try_parse("Lorem @file main.rs ", 0),
1153            Some(MentionCompletion {
1154                source_range: 6..19,
1155                mode: Some(ContextPickerMode::File),
1156                argument: Some("main.rs".to_string()),
1157            })
1158        );
1159
1160        assert_eq!(
1161            MentionCompletion::try_parse("Lorem @file main.rs Ipsum", 0),
1162            Some(MentionCompletion {
1163                source_range: 6..19,
1164                mode: Some(ContextPickerMode::File),
1165                argument: Some("main.rs".to_string()),
1166            })
1167        );
1168
1169        assert_eq!(
1170            MentionCompletion::try_parse("Lorem @main", 0),
1171            Some(MentionCompletion {
1172                source_range: 6..11,
1173                mode: None,
1174                argument: Some("main".to_string()),
1175            })
1176        );
1177
1178        assert_eq!(MentionCompletion::try_parse("test@", 0), None);
1179    }
1180
1181    struct MessageEditorItem(Entity<MessageEditor>);
1182
1183    impl Item for MessageEditorItem {
1184        type Event = ();
1185
1186        fn include_in_nav_history() -> bool {
1187            false
1188        }
1189
1190        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1191            "Test".into()
1192        }
1193    }
1194
1195    impl EventEmitter<()> for MessageEditorItem {}
1196
1197    impl Focusable for MessageEditorItem {
1198        fn focus_handle(&self, cx: &App) -> FocusHandle {
1199            self.0.read(cx).focus_handle(cx).clone()
1200        }
1201    }
1202
1203    impl Render for MessageEditorItem {
1204        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1205            self.0.clone().into_any_element()
1206        }
1207    }
1208
1209    #[gpui::test]
1210    async fn test_context_completion_provider(cx: &mut TestAppContext) {
1211        init_test(cx);
1212
1213        let app_state = cx.update(AppState::test);
1214
1215        cx.update(|cx| {
1216            language::init(cx);
1217            editor::init(cx);
1218            workspace::init(app_state.clone(), cx);
1219            Project::init_settings(cx);
1220        });
1221
1222        app_state
1223            .fs
1224            .as_fake()
1225            .insert_tree(
1226                path!("/dir"),
1227                json!({
1228                    "editor": "",
1229                    "a": {
1230                        "one.txt": "1",
1231                        "two.txt": "2",
1232                        "three.txt": "3",
1233                        "four.txt": "4"
1234                    },
1235                    "b": {
1236                        "five.txt": "5",
1237                        "six.txt": "6",
1238                        "seven.txt": "7",
1239                        "eight.txt": "8",
1240                    }
1241                }),
1242            )
1243            .await;
1244
1245        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
1246        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1247        let workspace = window.root(cx).unwrap();
1248
1249        let worktree = project.update(cx, |project, cx| {
1250            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
1251            assert_eq!(worktrees.len(), 1);
1252            worktrees.pop().unwrap()
1253        });
1254        let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1255
1256        let mut cx = VisualTestContext::from_window(*window.deref(), cx);
1257
1258        let paths = vec![
1259            path!("a/one.txt"),
1260            path!("a/two.txt"),
1261            path!("a/three.txt"),
1262            path!("a/four.txt"),
1263            path!("b/five.txt"),
1264            path!("b/six.txt"),
1265            path!("b/seven.txt"),
1266            path!("b/eight.txt"),
1267        ];
1268
1269        let mut opened_editors = Vec::new();
1270        for path in paths {
1271            let buffer = workspace
1272                .update_in(&mut cx, |workspace, window, cx| {
1273                    workspace.open_path(
1274                        ProjectPath {
1275                            worktree_id,
1276                            path: Path::new(path).into(),
1277                        },
1278                        None,
1279                        false,
1280                        window,
1281                        cx,
1282                    )
1283                })
1284                .await
1285                .unwrap();
1286            opened_editors.push(buffer);
1287        }
1288
1289        let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx));
1290        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
1291
1292        let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
1293            let workspace_handle = cx.weak_entity();
1294            let message_editor = cx.new(|cx| {
1295                MessageEditor::new(
1296                    workspace_handle,
1297                    project.clone(),
1298                    thread_store.clone(),
1299                    text_thread_store.clone(),
1300                    EditorMode::AutoHeight {
1301                        max_lines: None,
1302                        min_lines: 1,
1303                    },
1304                    window,
1305                    cx,
1306                )
1307            });
1308            workspace.active_pane().update(cx, |pane, cx| {
1309                pane.add_item(
1310                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
1311                    true,
1312                    true,
1313                    None,
1314                    window,
1315                    cx,
1316                );
1317            });
1318            message_editor.read(cx).focus_handle(cx).focus(window);
1319            let editor = message_editor.read(cx).editor().clone();
1320            (message_editor, editor)
1321        });
1322
1323        cx.simulate_input("Lorem ");
1324
1325        editor.update(&mut cx, |editor, cx| {
1326            assert_eq!(editor.text(cx), "Lorem ");
1327            assert!(!editor.has_visible_completions_menu());
1328        });
1329
1330        cx.simulate_input("@");
1331
1332        editor.update(&mut cx, |editor, cx| {
1333            assert_eq!(editor.text(cx), "Lorem @");
1334            assert!(editor.has_visible_completions_menu());
1335            assert_eq!(
1336                current_completion_labels(editor),
1337                &[
1338                    "eight.txt dir/b/",
1339                    "seven.txt dir/b/",
1340                    "six.txt dir/b/",
1341                    "five.txt dir/b/",
1342                    "Files & Directories",
1343                    "Symbols",
1344                    "Threads",
1345                    "Fetch"
1346                ]
1347            );
1348        });
1349
1350        // Select and confirm "File"
1351        editor.update_in(&mut cx, |editor, window, cx| {
1352            assert!(editor.has_visible_completions_menu());
1353            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1354            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1355            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1356            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1357            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1358        });
1359
1360        cx.run_until_parked();
1361
1362        editor.update(&mut cx, |editor, cx| {
1363            assert_eq!(editor.text(cx), "Lorem @file ");
1364            assert!(editor.has_visible_completions_menu());
1365        });
1366
1367        cx.simulate_input("one");
1368
1369        editor.update(&mut cx, |editor, cx| {
1370            assert_eq!(editor.text(cx), "Lorem @file one");
1371            assert!(editor.has_visible_completions_menu());
1372            assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
1373        });
1374
1375        editor.update_in(&mut cx, |editor, window, cx| {
1376            assert!(editor.has_visible_completions_menu());
1377            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1378        });
1379
1380        editor.update(&mut cx, |editor, cx| {
1381            assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) ");
1382            assert!(!editor.has_visible_completions_menu());
1383            assert_eq!(
1384                fold_ranges(editor, cx),
1385                vec![Point::new(0, 6)..Point::new(0, 39)]
1386            );
1387        });
1388
1389        let contents = message_editor
1390            .update_in(&mut cx, |message_editor, window, cx| {
1391                message_editor.mention_set().contents(
1392                    project.clone(),
1393                    thread_store.clone(),
1394                    text_thread_store.clone(),
1395                    window,
1396                    cx,
1397                )
1398            })
1399            .await
1400            .unwrap()
1401            .into_values()
1402            .collect::<Vec<_>>();
1403
1404        pretty_assertions::assert_eq!(
1405            contents,
1406            [Mention::Text {
1407                content: "1".into(),
1408                uri: "file:///dir/a/one.txt".parse().unwrap()
1409            }]
1410        );
1411
1412        cx.simulate_input(" ");
1413
1414        editor.update(&mut cx, |editor, cx| {
1415            assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt)  ");
1416            assert!(!editor.has_visible_completions_menu());
1417            assert_eq!(
1418                fold_ranges(editor, cx),
1419                vec![Point::new(0, 6)..Point::new(0, 39)]
1420            );
1421        });
1422
1423        cx.simulate_input("Ipsum ");
1424
1425        editor.update(&mut cx, |editor, cx| {
1426            assert_eq!(
1427                editor.text(cx),
1428                "Lorem [@one.txt](file:///dir/a/one.txt)  Ipsum ",
1429            );
1430            assert!(!editor.has_visible_completions_menu());
1431            assert_eq!(
1432                fold_ranges(editor, cx),
1433                vec![Point::new(0, 6)..Point::new(0, 39)]
1434            );
1435        });
1436
1437        cx.simulate_input("@file ");
1438
1439        editor.update(&mut cx, |editor, cx| {
1440            assert_eq!(
1441                editor.text(cx),
1442                "Lorem [@one.txt](file:///dir/a/one.txt)  Ipsum @file ",
1443            );
1444            assert!(editor.has_visible_completions_menu());
1445            assert_eq!(
1446                fold_ranges(editor, cx),
1447                vec![Point::new(0, 6)..Point::new(0, 39)]
1448            );
1449        });
1450
1451        editor.update_in(&mut cx, |editor, window, cx| {
1452            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1453        });
1454
1455        cx.run_until_parked();
1456
1457        let contents = message_editor
1458            .update_in(&mut cx, |message_editor, window, cx| {
1459                message_editor.mention_set().contents(
1460                    project.clone(),
1461                    thread_store.clone(),
1462                    text_thread_store.clone(),
1463                    window,
1464                    cx,
1465                )
1466            })
1467            .await
1468            .unwrap()
1469            .into_values()
1470            .collect::<Vec<_>>();
1471
1472        assert_eq!(contents.len(), 2);
1473        pretty_assertions::assert_eq!(
1474            contents[1],
1475            Mention::Text {
1476                content: "8".to_string(),
1477                uri: "file:///dir/b/eight.txt".parse().unwrap(),
1478            }
1479        );
1480
1481        editor.update(&mut cx, |editor, cx| {
1482            assert_eq!(
1483                editor.text(cx),
1484                "Lorem [@one.txt](file:///dir/a/one.txt)  Ipsum [@eight.txt](file:///dir/b/eight.txt) "
1485            );
1486            assert!(!editor.has_visible_completions_menu());
1487            assert_eq!(
1488                fold_ranges(editor, cx),
1489                vec![
1490                    Point::new(0, 6)..Point::new(0, 39),
1491                    Point::new(0, 47)..Point::new(0, 84)
1492                ]
1493            );
1494        });
1495
1496        let plain_text_language = Arc::new(language::Language::new(
1497            language::LanguageConfig {
1498                name: "Plain Text".into(),
1499                matcher: language::LanguageMatcher {
1500                    path_suffixes: vec!["txt".to_string()],
1501                    ..Default::default()
1502                },
1503                ..Default::default()
1504            },
1505            None,
1506        ));
1507
1508        // Register the language and fake LSP
1509        let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
1510        language_registry.add(plain_text_language);
1511
1512        let mut fake_language_servers = language_registry.register_fake_lsp(
1513            "Plain Text",
1514            language::FakeLspAdapter {
1515                capabilities: lsp::ServerCapabilities {
1516                    workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
1517                    ..Default::default()
1518                },
1519                ..Default::default()
1520            },
1521        );
1522
1523        // Open the buffer to trigger LSP initialization
1524        let buffer = project
1525            .update(&mut cx, |project, cx| {
1526                project.open_local_buffer(path!("/dir/a/one.txt"), cx)
1527            })
1528            .await
1529            .unwrap();
1530
1531        // Register the buffer with language servers
1532        let _handle = project.update(&mut cx, |project, cx| {
1533            project.register_buffer_with_language_servers(&buffer, cx)
1534        });
1535
1536        cx.run_until_parked();
1537
1538        let fake_language_server = fake_language_servers.next().await.unwrap();
1539        fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
1540            |_, _| async move {
1541                Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
1542                    #[allow(deprecated)]
1543                    lsp::SymbolInformation {
1544                        name: "MySymbol".into(),
1545                        location: lsp::Location {
1546                            uri: lsp::Url::from_file_path(path!("/dir/a/one.txt")).unwrap(),
1547                            range: lsp::Range::new(
1548                                lsp::Position::new(0, 0),
1549                                lsp::Position::new(0, 1),
1550                            ),
1551                        },
1552                        kind: lsp::SymbolKind::CONSTANT,
1553                        tags: None,
1554                        container_name: None,
1555                        deprecated: None,
1556                    },
1557                ])))
1558            },
1559        );
1560
1561        cx.simulate_input("@symbol ");
1562
1563        editor.update(&mut cx, |editor, cx| {
1564            assert_eq!(
1565                editor.text(cx),
1566                "Lorem [@one.txt](file:///dir/a/one.txt)  Ipsum [@eight.txt](file:///dir/b/eight.txt) @symbol "
1567            );
1568            assert!(editor.has_visible_completions_menu());
1569            assert_eq!(
1570                current_completion_labels(editor),
1571                &[
1572                    "MySymbol",
1573                ]
1574            );
1575        });
1576
1577        editor.update_in(&mut cx, |editor, window, cx| {
1578            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1579        });
1580
1581        let contents = message_editor
1582            .update_in(&mut cx, |message_editor, window, cx| {
1583                message_editor.mention_set().contents(
1584                    project.clone(),
1585                    thread_store,
1586                    text_thread_store,
1587                    window,
1588                    cx,
1589                )
1590            })
1591            .await
1592            .unwrap()
1593            .into_values()
1594            .collect::<Vec<_>>();
1595
1596        assert_eq!(contents.len(), 3);
1597        pretty_assertions::assert_eq!(
1598            contents[2],
1599            Mention::Text {
1600                content: "1".into(),
1601                uri: "file:///dir/a/one.txt?symbol=MySymbol#L1:1"
1602                    .parse()
1603                    .unwrap(),
1604            }
1605        );
1606
1607        cx.run_until_parked();
1608
1609        editor.read_with(&mut cx, |editor, cx| {
1610            assert_eq!(
1611                editor.text(cx),
1612                "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) "
1613            );
1614        });
1615    }
1616
1617    fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
1618        let snapshot = editor.buffer().read(cx).snapshot(cx);
1619        editor.display_map.update(cx, |display_map, cx| {
1620            display_map
1621                .snapshot(cx)
1622                .folds_in_range(0..snapshot.len())
1623                .map(|fold| fold.range.to_point(&snapshot))
1624                .collect()
1625        })
1626    }
1627
1628    fn current_completion_labels(editor: &Editor) -> Vec<String> {
1629        let completions = editor.current_completions().expect("Missing completions");
1630        completions
1631            .into_iter()
1632            .map(|completion| completion.label.text.to_string())
1633            .collect::<Vec<_>>()
1634    }
1635
1636    pub(crate) fn init_test(cx: &mut TestAppContext) {
1637        cx.update(|cx| {
1638            let store = SettingsStore::test(cx);
1639            cx.set_global(store);
1640            theme::init(theme::LoadThemes::JustBase, cx);
1641            client::init_settings(cx);
1642            language::init(cx);
1643            Project::init_settings(cx);
1644            workspace::init_settings(cx);
1645            editor::init_settings(cx);
1646        });
1647    }
1648}