completion_provider.rs

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