completion_provider.rs

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