completion_provider.rs

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