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