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