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