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