completion_provider.rs

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