completion_provider.rs

   1use std::cell::RefCell;
   2use std::ops::Range;
   3use std::path::{Path, PathBuf};
   4use std::rc::Rc;
   5use std::sync::Arc;
   6use std::sync::atomic::AtomicBool;
   7
   8use anyhow::Result;
   9use editor::{CompletionProvider, Editor, ExcerptId, ToOffset as _};
  10use file_icons::FileIcons;
  11use fuzzy::{StringMatch, StringMatchCandidate};
  12use gpui::{App, Entity, Task, WeakEntity};
  13use http_client::HttpClientWithUrl;
  14use itertools::Itertools;
  15use language::{Buffer, CodeLabel, HighlightId};
  16use lsp::CompletionContext;
  17use project::{Completion, CompletionIntent, ProjectPath, Symbol, WorktreeId};
  18use prompt_store::PromptStore;
  19use rope::Point;
  20use text::{Anchor, OffsetRangeExt, ToPoint};
  21use ui::prelude::*;
  22use util::ResultExt as _;
  23use workspace::Workspace;
  24
  25use crate::Thread;
  26use crate::context::{AgentContextHandle, AgentContextKey, ContextCreasesAddon, RULES_ICON};
  27use crate::context_store::ContextStore;
  28use crate::thread_store::{TextThreadStore, ThreadStore};
  29
  30use super::fetch_context_picker::fetch_url_content;
  31use super::file_context_picker::{FileMatch, search_files};
  32use super::rules_context_picker::{RulesContextEntry, search_rules};
  33use super::symbol_context_picker::SymbolMatch;
  34use super::symbol_context_picker::search_symbols;
  35use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
  36use super::{
  37    ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry,
  38    available_context_picker_entries, recent_context_picker_entries, selection_ranges,
  39};
  40
  41pub(crate) enum Match {
  42    File(FileMatch),
  43    Symbol(SymbolMatch),
  44    Thread(ThreadMatch),
  45    Fetch(SharedString),
  46    Rules(RulesContextEntry),
  47    Entry(EntryMatch),
  48}
  49
  50pub struct EntryMatch {
  51    mat: Option<StringMatch>,
  52    entry: ContextPickerEntry,
  53}
  54
  55impl Match {
  56    pub fn score(&self) -> f64 {
  57        match self {
  58            Match::File(file) => file.mat.score,
  59            Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
  60            Match::Thread(_) => 1.,
  61            Match::Symbol(_) => 1.,
  62            Match::Fetch(_) => 1.,
  63            Match::Rules(_) => 1.,
  64        }
  65    }
  66}
  67
  68fn search(
  69    mode: Option<ContextPickerMode>,
  70    query: String,
  71    cancellation_flag: Arc<AtomicBool>,
  72    recent_entries: Vec<RecentEntry>,
  73    prompt_store: Option<Entity<PromptStore>>,
  74    thread_store: Option<WeakEntity<ThreadStore>>,
  75    text_thread_context_store: Option<WeakEntity<assistant_context_editor::ContextStore>>,
  76    workspace: Entity<Workspace>,
  77    cx: &mut App,
  78) -> Task<Vec<Match>> {
  79    match mode {
  80        Some(ContextPickerMode::File) => {
  81            let search_files_task =
  82                search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
  83            cx.background_spawn(async move {
  84                search_files_task
  85                    .await
  86                    .into_iter()
  87                    .map(Match::File)
  88                    .collect()
  89            })
  90        }
  91
  92        Some(ContextPickerMode::Symbol) => {
  93            let search_symbols_task =
  94                search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx);
  95            cx.background_spawn(async move {
  96                search_symbols_task
  97                    .await
  98                    .into_iter()
  99                    .map(Match::Symbol)
 100                    .collect()
 101            })
 102        }
 103
 104        Some(ContextPickerMode::Thread) => {
 105            if let Some((thread_store, context_store)) = thread_store
 106                .as_ref()
 107                .and_then(|t| t.upgrade())
 108                .zip(text_thread_context_store.as_ref().and_then(|t| t.upgrade()))
 109            {
 110                let search_threads_task = search_threads(
 111                    query.clone(),
 112                    cancellation_flag.clone(),
 113                    thread_store,
 114                    context_store,
 115                    cx,
 116                );
 117                cx.background_spawn(async move {
 118                    search_threads_task
 119                        .await
 120                        .into_iter()
 121                        .map(Match::Thread)
 122                        .collect()
 123                })
 124            } else {
 125                Task::ready(Vec::new())
 126            }
 127        }
 128
 129        Some(ContextPickerMode::Fetch) => {
 130            if !query.is_empty() {
 131                Task::ready(vec![Match::Fetch(query.into())])
 132            } else {
 133                Task::ready(Vec::new())
 134            }
 135        }
 136
 137        Some(ContextPickerMode::Rules) => {
 138            if let Some(prompt_store) = prompt_store.as_ref() {
 139                let search_rules_task =
 140                    search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx);
 141                cx.background_spawn(async move {
 142                    search_rules_task
 143                        .await
 144                        .into_iter()
 145                        .map(Match::Rules)
 146                        .collect::<Vec<_>>()
 147                })
 148            } else {
 149                Task::ready(Vec::new())
 150            }
 151        }
 152
 153        None => {
 154            if query.is_empty() {
 155                let mut matches = recent_entries
 156                    .into_iter()
 157                    .map(|entry| match entry {
 158                        super::RecentEntry::File {
 159                            project_path,
 160                            path_prefix,
 161                        } => Match::File(FileMatch {
 162                            mat: fuzzy::PathMatch {
 163                                score: 1.,
 164                                positions: Vec::new(),
 165                                worktree_id: project_path.worktree_id.to_usize(),
 166                                path: project_path.path,
 167                                path_prefix,
 168                                is_dir: false,
 169                                distance_to_relative_ancestor: 0,
 170                            },
 171                            is_recent: true,
 172                        }),
 173                        super::RecentEntry::Thread(thread_context_entry) => {
 174                            Match::Thread(ThreadMatch {
 175                                thread: thread_context_entry,
 176                                is_recent: true,
 177                            })
 178                        }
 179                    })
 180                    .collect::<Vec<_>>();
 181
 182                matches.extend(
 183                    available_context_picker_entries(&prompt_store, &thread_store, &workspace, cx)
 184                        .into_iter()
 185                        .map(|mode| {
 186                            Match::Entry(EntryMatch {
 187                                entry: mode,
 188                                mat: None,
 189                            })
 190                        }),
 191                );
 192
 193                Task::ready(matches)
 194            } else {
 195                let executor = cx.background_executor().clone();
 196
 197                let search_files_task =
 198                    search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
 199
 200                let entries =
 201                    available_context_picker_entries(&prompt_store, &thread_store, &workspace, cx);
 202                let entry_candidates = entries
 203                    .iter()
 204                    .enumerate()
 205                    .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword()))
 206                    .collect::<Vec<_>>();
 207
 208                cx.background_spawn(async move {
 209                    let mut matches = search_files_task
 210                        .await
 211                        .into_iter()
 212                        .map(Match::File)
 213                        .collect::<Vec<_>>();
 214
 215                    let entry_matches = fuzzy::match_strings(
 216                        &entry_candidates,
 217                        &query,
 218                        false,
 219                        100,
 220                        &Arc::new(AtomicBool::default()),
 221                        executor,
 222                    )
 223                    .await;
 224
 225                    matches.extend(entry_matches.into_iter().map(|mat| {
 226                        Match::Entry(EntryMatch {
 227                            entry: entries[mat.candidate_id],
 228                            mat: Some(mat),
 229                        })
 230                    }));
 231
 232                    matches.sort_by(|a, b| {
 233                        b.score()
 234                            .partial_cmp(&a.score())
 235                            .unwrap_or(std::cmp::Ordering::Equal)
 236                    });
 237
 238                    matches
 239                })
 240            }
 241        }
 242    }
 243}
 244
 245pub struct ContextPickerCompletionProvider {
 246    workspace: WeakEntity<Workspace>,
 247    context_store: WeakEntity<ContextStore>,
 248    thread_store: Option<WeakEntity<ThreadStore>>,
 249    text_thread_store: Option<WeakEntity<TextThreadStore>>,
 250    editor: WeakEntity<Editor>,
 251    excluded_buffer: Option<WeakEntity<Buffer>>,
 252}
 253
 254impl ContextPickerCompletionProvider {
 255    pub fn new(
 256        workspace: WeakEntity<Workspace>,
 257        context_store: WeakEntity<ContextStore>,
 258        thread_store: Option<WeakEntity<ThreadStore>>,
 259        text_thread_store: Option<WeakEntity<TextThreadStore>>,
 260        editor: WeakEntity<Editor>,
 261        exclude_buffer: Option<WeakEntity<Buffer>>,
 262    ) -> Self {
 263        Self {
 264            workspace,
 265            context_store,
 266            thread_store,
 267            text_thread_store,
 268            editor,
 269            excluded_buffer: exclude_buffer,
 270        }
 271    }
 272
 273    fn completion_for_entry(
 274        entry: ContextPickerEntry,
 275        excerpt_id: ExcerptId,
 276        source_range: Range<Anchor>,
 277        editor: Entity<Editor>,
 278        context_store: Entity<ContextStore>,
 279        workspace: &Entity<Workspace>,
 280        cx: &mut App,
 281    ) -> Option<Completion> {
 282        match entry {
 283            ContextPickerEntry::Mode(mode) => Some(Completion {
 284                replace_range: source_range.clone(),
 285                new_text: format!("@{} ", mode.keyword()),
 286                label: CodeLabel::plain(mode.label().to_string(), None),
 287                icon_path: Some(mode.icon().path().into()),
 288                documentation: None,
 289                source: project::CompletionSource::Custom,
 290                insert_text_mode: None,
 291                // This ensures that when a user accepts this completion, the
 292                // completion menu will still be shown after "@category " is
 293                // inserted
 294                confirm: Some(Arc::new(|_, _, _| true)),
 295            }),
 296            ContextPickerEntry::Action(action) => {
 297                let (new_text, on_action) = match action {
 298                    ContextPickerAction::AddSelections => {
 299                        let selections = selection_ranges(workspace, cx);
 300
 301                        let selection_infos = selections
 302                            .iter()
 303                            .map(|(buffer, range)| {
 304                                let full_path = buffer
 305                                    .read(cx)
 306                                    .file()
 307                                    .map(|file| file.full_path(cx))
 308                                    .unwrap_or_else(|| PathBuf::from("untitled"));
 309                                let file_name = full_path
 310                                    .file_name()
 311                                    .unwrap_or_default()
 312                                    .to_string_lossy()
 313                                    .to_string();
 314                                let line_range = range.to_point(&buffer.read(cx).snapshot());
 315
 316                                let link = MentionLink::for_selection(
 317                                    &file_name,
 318                                    &full_path.to_string_lossy(),
 319                                    line_range.start.row as usize..line_range.end.row as usize,
 320                                );
 321                                (file_name, link, line_range)
 322                            })
 323                            .collect::<Vec<_>>();
 324
 325                        let new_text = selection_infos.iter().map(|(_, link, _)| link).join(" ");
 326
 327                        let callback = Arc::new({
 328                            let context_store = context_store.clone();
 329                            let selections = selections.clone();
 330                            let selection_infos = selection_infos.clone();
 331                            move |_, window: &mut Window, cx: &mut App| {
 332                                context_store.update(cx, |context_store, cx| {
 333                                    for (buffer, range) in &selections {
 334                                        context_store.add_selection(
 335                                            buffer.clone(),
 336                                            range.clone(),
 337                                            cx,
 338                                        );
 339                                    }
 340                                });
 341
 342                                let editor = editor.clone();
 343                                let selection_infos = selection_infos.clone();
 344                                window.defer(cx, move |window, cx| {
 345                                    let mut current_offset = 0;
 346                                    for (file_name, link, line_range) in selection_infos.iter() {
 347                                        let snapshot =
 348                                            editor.read(cx).buffer().read(cx).snapshot(cx);
 349                                        let Some(start) = snapshot
 350                                            .anchor_in_excerpt(excerpt_id, source_range.start)
 351                                        else {
 352                                            return;
 353                                        };
 354
 355                                        let offset = start.to_offset(&snapshot) + current_offset;
 356                                        let text_len = link.len();
 357
 358                                        let range = snapshot.anchor_after(offset)
 359                                            ..snapshot.anchor_after(offset + text_len);
 360
 361                                        let crease = super::crease_for_mention(
 362                                            format!(
 363                                                "{} ({}-{})",
 364                                                file_name,
 365                                                line_range.start.row + 1,
 366                                                line_range.end.row + 1
 367                                            )
 368                                            .into(),
 369                                            IconName::Context.path().into(),
 370                                            range,
 371                                            editor.downgrade(),
 372                                        );
 373
 374                                        editor.update(cx, |editor, cx| {
 375                                            editor.insert_creases(vec![crease.clone()], cx);
 376                                            editor.fold_creases(vec![crease], false, window, cx);
 377                                        });
 378
 379                                        current_offset += text_len + 1;
 380                                    }
 381                                });
 382
 383                                false
 384                            }
 385                        });
 386
 387                        (new_text, callback)
 388                    }
 389                };
 390
 391                Some(Completion {
 392                    replace_range: source_range.clone(),
 393                    new_text,
 394                    label: CodeLabel::plain(action.label().to_string(), None),
 395                    icon_path: Some(action.icon().path().into()),
 396                    documentation: None,
 397                    source: project::CompletionSource::Custom,
 398                    insert_text_mode: None,
 399                    // This ensures that when a user accepts this completion, the
 400                    // completion menu will still be shown after "@category " is
 401                    // inserted
 402                    confirm: Some(on_action),
 403                })
 404            }
 405        }
 406    }
 407
 408    fn completion_for_thread(
 409        thread_entry: ThreadContextEntry,
 410        excerpt_id: ExcerptId,
 411        source_range: Range<Anchor>,
 412        recent: bool,
 413        editor: Entity<Editor>,
 414        context_store: Entity<ContextStore>,
 415        thread_store: Entity<ThreadStore>,
 416        text_thread_store: Entity<TextThreadStore>,
 417    ) -> Completion {
 418        let icon_for_completion = if recent {
 419            IconName::HistoryRerun
 420        } else {
 421            IconName::MessageBubbles
 422        };
 423        let new_text = MentionLink::for_thread(&thread_entry);
 424        let new_text_len = new_text.len();
 425        Completion {
 426            replace_range: source_range.clone(),
 427            new_text,
 428            label: CodeLabel::plain(thread_entry.title().to_string(), None),
 429            documentation: None,
 430            insert_text_mode: None,
 431            source: project::CompletionSource::Custom,
 432            icon_path: Some(icon_for_completion.path().into()),
 433            confirm: Some(confirm_completion_callback(
 434                IconName::MessageBubbles.path().into(),
 435                thread_entry.title().clone(),
 436                excerpt_id,
 437                source_range.start,
 438                new_text_len,
 439                editor.clone(),
 440                context_store.clone(),
 441                move |window, cx| match &thread_entry {
 442                    ThreadContextEntry::Thread { id, .. } => {
 443                        let thread_id = id.clone();
 444                        let context_store = context_store.clone();
 445                        let thread_store = thread_store.clone();
 446                        window.spawn::<_, Option<_>>(cx, async move |cx| {
 447                            let thread: Entity<Thread> = thread_store
 448                                .update_in(cx, |thread_store, window, cx| {
 449                                    thread_store.open_thread(&thread_id, window, cx)
 450                                })
 451                                .ok()?
 452                                .await
 453                                .log_err()?;
 454                            let context = context_store
 455                                .update(cx, |context_store, cx| {
 456                                    context_store.add_thread(thread, false, cx)
 457                                })
 458                                .ok()??;
 459                            Some(context)
 460                        })
 461                    }
 462                    ThreadContextEntry::Context { path, .. } => {
 463                        let path = path.clone();
 464                        let context_store = context_store.clone();
 465                        let text_thread_store = text_thread_store.clone();
 466                        cx.spawn::<_, Option<_>>(async move |cx| {
 467                            let thread = text_thread_store
 468                                .update(cx, |store, cx| store.open_local_context(path, cx))
 469                                .ok()?
 470                                .await
 471                                .log_err()?;
 472                            let context = context_store
 473                                .update(cx, |context_store, cx| {
 474                                    context_store.add_text_thread(thread, false, cx)
 475                                })
 476                                .ok()??;
 477                            Some(context)
 478                        })
 479                    }
 480                },
 481            )),
 482        }
 483    }
 484
 485    fn completion_for_rules(
 486        rules: RulesContextEntry,
 487        excerpt_id: ExcerptId,
 488        source_range: Range<Anchor>,
 489        editor: Entity<Editor>,
 490        context_store: Entity<ContextStore>,
 491    ) -> Completion {
 492        let new_text = MentionLink::for_rule(&rules);
 493        let new_text_len = new_text.len();
 494        Completion {
 495            replace_range: source_range.clone(),
 496            new_text,
 497            label: CodeLabel::plain(rules.title.to_string(), None),
 498            documentation: None,
 499            insert_text_mode: None,
 500            source: project::CompletionSource::Custom,
 501            icon_path: Some(RULES_ICON.path().into()),
 502            confirm: Some(confirm_completion_callback(
 503                RULES_ICON.path().into(),
 504                rules.title.clone(),
 505                excerpt_id,
 506                source_range.start,
 507                new_text_len,
 508                editor.clone(),
 509                context_store.clone(),
 510                move |_, cx| {
 511                    let user_prompt_id = rules.prompt_id;
 512                    let context = context_store.update(cx, |context_store, cx| {
 513                        context_store.add_rules(user_prompt_id, false, cx)
 514                    });
 515                    Task::ready(context)
 516                },
 517            )),
 518        }
 519    }
 520
 521    fn completion_for_fetch(
 522        source_range: Range<Anchor>,
 523        url_to_fetch: SharedString,
 524        excerpt_id: ExcerptId,
 525        editor: Entity<Editor>,
 526        context_store: Entity<ContextStore>,
 527        http_client: Arc<HttpClientWithUrl>,
 528    ) -> Completion {
 529        let new_text = MentionLink::for_fetch(&url_to_fetch);
 530        let new_text_len = new_text.len();
 531        Completion {
 532            replace_range: source_range.clone(),
 533            new_text,
 534            label: CodeLabel::plain(url_to_fetch.to_string(), None),
 535            documentation: None,
 536            source: project::CompletionSource::Custom,
 537            icon_path: Some(IconName::Globe.path().into()),
 538            insert_text_mode: None,
 539            confirm: Some(confirm_completion_callback(
 540                IconName::Globe.path().into(),
 541                url_to_fetch.clone(),
 542                excerpt_id,
 543                source_range.start,
 544                new_text_len,
 545                editor.clone(),
 546                context_store.clone(),
 547                move |_, cx| {
 548                    let context_store = context_store.clone();
 549                    let http_client = http_client.clone();
 550                    let url_to_fetch = url_to_fetch.clone();
 551                    cx.spawn(async move |cx| {
 552                        if let Some(context) = context_store
 553                            .update(cx, |context_store, _| {
 554                                context_store.get_url_context(url_to_fetch.clone())
 555                            })
 556                            .ok()?
 557                        {
 558                            return Some(context);
 559                        }
 560                        let content = cx
 561                            .background_spawn(fetch_url_content(
 562                                http_client,
 563                                url_to_fetch.to_string(),
 564                            ))
 565                            .await
 566                            .log_err()?;
 567                        context_store
 568                            .update(cx, |context_store, cx| {
 569                                context_store.add_fetched_url(url_to_fetch.to_string(), content, cx)
 570                            })
 571                            .ok()
 572                    })
 573                },
 574            )),
 575        }
 576    }
 577
 578    fn completion_for_path(
 579        project_path: ProjectPath,
 580        path_prefix: &str,
 581        is_recent: bool,
 582        is_directory: bool,
 583        excerpt_id: ExcerptId,
 584        source_range: Range<Anchor>,
 585        editor: Entity<Editor>,
 586        context_store: Entity<ContextStore>,
 587        cx: &App,
 588    ) -> Completion {
 589        let (file_name, directory) = super::file_context_picker::extract_file_name_and_directory(
 590            &project_path.path,
 591            path_prefix,
 592        );
 593
 594        let label =
 595            build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
 596        let full_path = if let Some(directory) = directory {
 597            format!("{}{}", directory, file_name)
 598        } else {
 599            file_name.to_string()
 600        };
 601
 602        let crease_icon_path = if is_directory {
 603            FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into())
 604        } else {
 605            FileIcons::get_icon(Path::new(&full_path), cx)
 606                .unwrap_or_else(|| IconName::File.path().into())
 607        };
 608        let completion_icon_path = if is_recent {
 609            IconName::HistoryRerun.path().into()
 610        } else {
 611            crease_icon_path.clone()
 612        };
 613
 614        let new_text = MentionLink::for_file(&file_name, &full_path);
 615        let new_text_len = new_text.len();
 616        Completion {
 617            replace_range: source_range.clone(),
 618            new_text,
 619            label,
 620            documentation: None,
 621            source: project::CompletionSource::Custom,
 622            icon_path: Some(completion_icon_path),
 623            insert_text_mode: None,
 624            confirm: Some(confirm_completion_callback(
 625                crease_icon_path,
 626                file_name,
 627                excerpt_id,
 628                source_range.start,
 629                new_text_len,
 630                editor,
 631                context_store.clone(),
 632                move |_, cx| {
 633                    if is_directory {
 634                        Task::ready(
 635                            context_store
 636                                .update(cx, |context_store, cx| {
 637                                    context_store.add_directory(&project_path, false, cx)
 638                                })
 639                                .log_err()
 640                                .flatten(),
 641                        )
 642                    } else {
 643                        let result = context_store.update(cx, |context_store, cx| {
 644                            context_store.add_file_from_path(project_path.clone(), false, cx)
 645                        });
 646                        cx.spawn(async move |_| result.await.log_err().flatten())
 647                    }
 648                },
 649            )),
 650        }
 651    }
 652
 653    fn completion_for_symbol(
 654        symbol: Symbol,
 655        excerpt_id: ExcerptId,
 656        source_range: Range<Anchor>,
 657        editor: Entity<Editor>,
 658        context_store: Entity<ContextStore>,
 659        workspace: Entity<Workspace>,
 660        cx: &mut App,
 661    ) -> Option<Completion> {
 662        let path_prefix = workspace
 663            .read(cx)
 664            .project()
 665            .read(cx)
 666            .worktree_for_id(symbol.path.worktree_id, cx)?
 667            .read(cx)
 668            .root_name();
 669
 670        let (file_name, directory) = super::file_context_picker::extract_file_name_and_directory(
 671            &symbol.path.path,
 672            path_prefix,
 673        );
 674        let full_path = if let Some(directory) = directory {
 675            format!("{}{}", directory, file_name)
 676        } else {
 677            file_name.to_string()
 678        };
 679
 680        let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
 681        let mut label = CodeLabel::plain(symbol.name.clone(), None);
 682        label.push_str(" ", None);
 683        label.push_str(&file_name, comment_id);
 684
 685        let new_text = MentionLink::for_symbol(&symbol.name, &full_path);
 686        let new_text_len = new_text.len();
 687        Some(Completion {
 688            replace_range: source_range.clone(),
 689            new_text,
 690            label,
 691            documentation: None,
 692            source: project::CompletionSource::Custom,
 693            icon_path: Some(IconName::Code.path().into()),
 694            insert_text_mode: None,
 695            confirm: Some(confirm_completion_callback(
 696                IconName::Code.path().into(),
 697                symbol.name.clone().into(),
 698                excerpt_id,
 699                source_range.start,
 700                new_text_len,
 701                editor.clone(),
 702                context_store.clone(),
 703                move |_, cx| {
 704                    let symbol = symbol.clone();
 705                    let context_store = context_store.clone();
 706                    let workspace = workspace.clone();
 707                    let result = super::symbol_context_picker::add_symbol(
 708                        symbol.clone(),
 709                        false,
 710                        workspace.clone(),
 711                        context_store.downgrade(),
 712                        cx,
 713                    );
 714                    cx.spawn(async move |_| result.await.log_err()?.0)
 715                },
 716            )),
 717        })
 718    }
 719}
 720
 721fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
 722    let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
 723    let mut label = CodeLabel::default();
 724
 725    label.push_str(&file_name, None);
 726    label.push_str(" ", None);
 727
 728    if let Some(directory) = directory {
 729        label.push_str(&directory, comment_id);
 730    }
 731
 732    label.filter_range = 0..label.text().len();
 733
 734    label
 735}
 736
 737impl CompletionProvider for ContextPickerCompletionProvider {
 738    fn completions(
 739        &self,
 740        excerpt_id: ExcerptId,
 741        buffer: &Entity<Buffer>,
 742        buffer_position: Anchor,
 743        _trigger: CompletionContext,
 744        _window: &mut Window,
 745        cx: &mut Context<Editor>,
 746    ) -> Task<Result<Option<Vec<Completion>>>> {
 747        let state = buffer.update(cx, |buffer, _cx| {
 748            let position = buffer_position.to_point(buffer);
 749            let line_start = Point::new(position.row, 0);
 750            let offset_to_line = buffer.point_to_offset(line_start);
 751            let mut lines = buffer.text_for_range(line_start..position).lines();
 752            let line = lines.next()?;
 753            MentionCompletion::try_parse(line, offset_to_line)
 754        });
 755        let Some(state) = state else {
 756            return Task::ready(Ok(None));
 757        };
 758
 759        let Some((workspace, context_store)) =
 760            self.workspace.upgrade().zip(self.context_store.upgrade())
 761        else {
 762            return Task::ready(Ok(None));
 763        };
 764
 765        let snapshot = buffer.read(cx).snapshot();
 766        let source_range = snapshot.anchor_before(state.source_range.start)
 767            ..snapshot.anchor_before(state.source_range.end);
 768
 769        let thread_store = self.thread_store.clone();
 770        let text_thread_store = self.text_thread_store.clone();
 771        let editor = self.editor.clone();
 772        let http_client = workspace.read(cx).client().http_client();
 773
 774        let MentionCompletion { mode, argument, .. } = state;
 775        let query = argument.unwrap_or_else(|| "".to_string());
 776
 777        let excluded_path = self
 778            .excluded_buffer
 779            .as_ref()
 780            .and_then(WeakEntity::upgrade)
 781            .and_then(|b| b.read(cx).file())
 782            .map(|file| ProjectPath::from_file(file.as_ref(), cx));
 783
 784        let recent_entries = recent_context_picker_entries(
 785            context_store.clone(),
 786            thread_store.clone(),
 787            text_thread_store.clone(),
 788            workspace.clone(),
 789            excluded_path.clone(),
 790            cx,
 791        );
 792
 793        let prompt_store = thread_store.as_ref().and_then(|thread_store| {
 794            thread_store
 795                .read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone())
 796                .ok()
 797                .flatten()
 798        });
 799
 800        let search_task = search(
 801            mode,
 802            query,
 803            Arc::<AtomicBool>::default(),
 804            recent_entries,
 805            prompt_store,
 806            thread_store.clone(),
 807            text_thread_store.clone(),
 808            workspace.clone(),
 809            cx,
 810        );
 811
 812        cx.spawn(async move |_, cx| {
 813            let matches = search_task.await;
 814            let Some(editor) = editor.upgrade() else {
 815                return Ok(None);
 816            };
 817
 818            Ok(Some(cx.update(|cx| {
 819                matches
 820                    .into_iter()
 821                    .filter_map(|mat| match mat {
 822                        Match::File(FileMatch { mat, is_recent }) => {
 823                            let project_path = ProjectPath {
 824                                worktree_id: WorktreeId::from_usize(mat.worktree_id),
 825                                path: mat.path.clone(),
 826                            };
 827
 828                            if excluded_path.as_ref() == Some(&project_path) {
 829                                return None;
 830                            }
 831
 832                            Some(Self::completion_for_path(
 833                                project_path,
 834                                &mat.path_prefix,
 835                                is_recent,
 836                                mat.is_dir,
 837                                excerpt_id,
 838                                source_range.clone(),
 839                                editor.clone(),
 840                                context_store.clone(),
 841                                cx,
 842                            ))
 843                        }
 844
 845                        Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
 846                            symbol,
 847                            excerpt_id,
 848                            source_range.clone(),
 849                            editor.clone(),
 850                            context_store.clone(),
 851                            workspace.clone(),
 852                            cx,
 853                        ),
 854
 855                        Match::Thread(ThreadMatch {
 856                            thread, is_recent, ..
 857                        }) => {
 858                            let thread_store = thread_store.as_ref().and_then(|t| t.upgrade())?;
 859                            let text_thread_store =
 860                                text_thread_store.as_ref().and_then(|t| t.upgrade())?;
 861                            Some(Self::completion_for_thread(
 862                                thread,
 863                                excerpt_id,
 864                                source_range.clone(),
 865                                is_recent,
 866                                editor.clone(),
 867                                context_store.clone(),
 868                                thread_store,
 869                                text_thread_store,
 870                            ))
 871                        }
 872
 873                        Match::Rules(user_rules) => Some(Self::completion_for_rules(
 874                            user_rules,
 875                            excerpt_id,
 876                            source_range.clone(),
 877                            editor.clone(),
 878                            context_store.clone(),
 879                        )),
 880
 881                        Match::Fetch(url) => Some(Self::completion_for_fetch(
 882                            source_range.clone(),
 883                            url,
 884                            excerpt_id,
 885                            editor.clone(),
 886                            context_store.clone(),
 887                            http_client.clone(),
 888                        )),
 889
 890                        Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
 891                            entry,
 892                            excerpt_id,
 893                            source_range.clone(),
 894                            editor.clone(),
 895                            context_store.clone(),
 896                            &workspace,
 897                            cx,
 898                        ),
 899                    })
 900                    .collect()
 901            })?))
 902        })
 903    }
 904
 905    fn resolve_completions(
 906        &self,
 907        _buffer: Entity<Buffer>,
 908        _completion_indices: Vec<usize>,
 909        _completions: Rc<RefCell<Box<[Completion]>>>,
 910        _cx: &mut Context<Editor>,
 911    ) -> Task<Result<bool>> {
 912        Task::ready(Ok(true))
 913    }
 914
 915    fn is_completion_trigger(
 916        &self,
 917        buffer: &Entity<language::Buffer>,
 918        position: language::Anchor,
 919        _: &str,
 920        _: 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;
1070    use util::{path, separator};
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            separator!("a/one.txt"),
1222            separator!("a/two.txt"),
1223            separator!("a/three.txt"),
1224            separator!("a/four.txt"),
1225            separator!("b/five.txt"),
1226            separator!("b/six.txt"),
1227            separator!("b/seven.txt"),
1228            separator!("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(Box::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, 44)..Point::new(0, 79)
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, 44)..Point::new(0, 79)
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, 44)..Point::new(0, 79),
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}