completion_provider.rs

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