completion_provider.rs

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