completion_provider.rs

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