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