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