completion_provider.rs

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