completion_provider.rs

   1use std::cell::Cell;
   2use std::ops::Range;
   3use std::rc::Rc;
   4use std::sync::Arc;
   5use std::sync::atomic::AtomicBool;
   6
   7use acp_thread::MentionUri;
   8use agent_client_protocol as acp;
   9use agent2::{HistoryEntry, HistoryStore};
  10use anyhow::Result;
  11use editor::{CompletionProvider, Editor, ExcerptId};
  12use fuzzy::{StringMatch, StringMatchCandidate};
  13use gpui::{App, Entity, Task, WeakEntity};
  14use language::{Buffer, CodeLabel, HighlightId};
  15use lsp::CompletionContext;
  16use project::{
  17    Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId,
  18};
  19use prompt_store::PromptStore;
  20use rope::Point;
  21use text::{Anchor, ToPoint as _};
  22use ui::prelude::*;
  23use workspace::Workspace;
  24
  25use crate::AgentPanel;
  26use crate::acp::message_editor::MessageEditor;
  27use crate::context_picker::file_context_picker::{FileMatch, search_files};
  28use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules};
  29use crate::context_picker::symbol_context_picker::SymbolMatch;
  30use crate::context_picker::symbol_context_picker::search_symbols;
  31use crate::context_picker::{
  32    ContextPickerAction, ContextPickerEntry, ContextPickerMode, selection_ranges,
  33};
  34
  35pub(crate) enum Match {
  36    File(FileMatch),
  37    Symbol(SymbolMatch),
  38    Thread(HistoryEntry),
  39    RecentThread(HistoryEntry),
  40    Fetch(SharedString),
  41    Rules(RulesContextEntry),
  42    Entry(EntryMatch),
  43}
  44
  45pub struct EntryMatch {
  46    mat: Option<StringMatch>,
  47    entry: ContextPickerEntry,
  48}
  49
  50impl Match {
  51    pub fn score(&self) -> f64 {
  52        match self {
  53            Match::File(file) => file.mat.score,
  54            Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
  55            Match::Thread(_) => 1.,
  56            Match::RecentThread(_) => 1.,
  57            Match::Symbol(_) => 1.,
  58            Match::Rules(_) => 1.,
  59            Match::Fetch(_) => 1.,
  60        }
  61    }
  62}
  63
  64pub struct ContextPickerCompletionProvider {
  65    message_editor: WeakEntity<MessageEditor>,
  66    workspace: WeakEntity<Workspace>,
  67    history_store: Entity<HistoryStore>,
  68    prompt_store: Option<Entity<PromptStore>>,
  69    prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
  70}
  71
  72impl ContextPickerCompletionProvider {
  73    pub fn new(
  74        message_editor: WeakEntity<MessageEditor>,
  75        workspace: WeakEntity<Workspace>,
  76        history_store: Entity<HistoryStore>,
  77        prompt_store: Option<Entity<PromptStore>>,
  78        prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
  79    ) -> Self {
  80        Self {
  81            message_editor,
  82            workspace,
  83            history_store,
  84            prompt_store,
  85            prompt_capabilities,
  86        }
  87    }
  88
  89    fn completion_for_entry(
  90        entry: ContextPickerEntry,
  91        source_range: Range<Anchor>,
  92        message_editor: WeakEntity<MessageEditor>,
  93        workspace: &Entity<Workspace>,
  94        cx: &mut App,
  95    ) -> Option<Completion> {
  96        match entry {
  97            ContextPickerEntry::Mode(mode) => Some(Completion {
  98                replace_range: source_range,
  99                new_text: format!("@{} ", mode.keyword()),
 100                label: CodeLabel::plain(mode.label().to_string(), None),
 101                icon_path: Some(mode.icon().path().into()),
 102                documentation: None,
 103                source: project::CompletionSource::Custom,
 104                insert_text_mode: None,
 105                // This ensures that when a user accepts this completion, the
 106                // completion menu will still be shown after "@category " is
 107                // inserted
 108                confirm: Some(Arc::new(|_, _, _| true)),
 109            }),
 110            ContextPickerEntry::Action(action) => {
 111                Self::completion_for_action(action, source_range, message_editor, workspace, cx)
 112            }
 113        }
 114    }
 115
 116    fn completion_for_thread(
 117        thread_entry: HistoryEntry,
 118        source_range: Range<Anchor>,
 119        recent: bool,
 120        editor: WeakEntity<MessageEditor>,
 121        cx: &mut App,
 122    ) -> Completion {
 123        let uri = thread_entry.mention_uri();
 124
 125        let icon_for_completion = if recent {
 126            IconName::HistoryRerun.path().into()
 127        } else {
 128            uri.icon_path(cx)
 129        };
 130
 131        let new_text = format!("{} ", uri.as_link());
 132
 133        let new_text_len = new_text.len();
 134        Completion {
 135            replace_range: source_range.clone(),
 136            new_text,
 137            label: CodeLabel::plain(thread_entry.title().to_string(), None),
 138            documentation: None,
 139            insert_text_mode: None,
 140            source: project::CompletionSource::Custom,
 141            icon_path: Some(icon_for_completion),
 142            confirm: Some(confirm_completion_callback(
 143                thread_entry.title().clone(),
 144                source_range.start,
 145                new_text_len - 1,
 146                editor,
 147                uri,
 148            )),
 149        }
 150    }
 151
 152    fn completion_for_rules(
 153        rule: RulesContextEntry,
 154        source_range: Range<Anchor>,
 155        editor: WeakEntity<MessageEditor>,
 156        cx: &mut App,
 157    ) -> Completion {
 158        let uri = MentionUri::Rule {
 159            id: rule.prompt_id.into(),
 160            name: rule.title.to_string(),
 161        };
 162        let new_text = format!("{} ", uri.as_link());
 163        let new_text_len = new_text.len();
 164        let icon_path = uri.icon_path(cx);
 165        Completion {
 166            replace_range: source_range.clone(),
 167            new_text,
 168            label: CodeLabel::plain(rule.title.to_string(), None),
 169            documentation: None,
 170            insert_text_mode: None,
 171            source: project::CompletionSource::Custom,
 172            icon_path: Some(icon_path),
 173            confirm: Some(confirm_completion_callback(
 174                rule.title,
 175                source_range.start,
 176                new_text_len - 1,
 177                editor,
 178                uri,
 179            )),
 180        }
 181    }
 182
 183    pub(crate) fn completion_for_path(
 184        project_path: ProjectPath,
 185        path_prefix: &str,
 186        is_recent: bool,
 187        is_directory: bool,
 188        source_range: Range<Anchor>,
 189        message_editor: WeakEntity<MessageEditor>,
 190        project: Entity<Project>,
 191        cx: &mut App,
 192    ) -> Option<Completion> {
 193        let (file_name, directory) =
 194            crate::context_picker::file_context_picker::extract_file_name_and_directory(
 195                &project_path.path,
 196                path_prefix,
 197            );
 198
 199        let label =
 200            build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
 201
 202        let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
 203
 204        let uri = if is_directory {
 205            MentionUri::Directory { abs_path }
 206        } else {
 207            MentionUri::File { abs_path }
 208        };
 209
 210        let crease_icon_path = uri.icon_path(cx);
 211        let completion_icon_path = if is_recent {
 212            IconName::HistoryRerun.path().into()
 213        } else {
 214            crease_icon_path
 215        };
 216
 217        let new_text = format!("{} ", uri.as_link());
 218        let new_text_len = new_text.len();
 219        Some(Completion {
 220            replace_range: source_range.clone(),
 221            new_text,
 222            label,
 223            documentation: None,
 224            source: project::CompletionSource::Custom,
 225            icon_path: Some(completion_icon_path),
 226            insert_text_mode: None,
 227            confirm: Some(confirm_completion_callback(
 228                file_name,
 229                source_range.start,
 230                new_text_len - 1,
 231                message_editor,
 232                uri,
 233            )),
 234        })
 235    }
 236
 237    fn completion_for_symbol(
 238        symbol: Symbol,
 239        source_range: Range<Anchor>,
 240        message_editor: WeakEntity<MessageEditor>,
 241        workspace: Entity<Workspace>,
 242        cx: &mut App,
 243    ) -> Option<Completion> {
 244        let project = workspace.read(cx).project().clone();
 245
 246        let label = CodeLabel::plain(symbol.name.clone(), None);
 247
 248        let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?;
 249        let uri = MentionUri::Symbol {
 250            abs_path,
 251            name: symbol.name.clone(),
 252            line_range: symbol.range.start.0.row..=symbol.range.end.0.row,
 253        };
 254        let new_text = format!("{} ", uri.as_link());
 255        let new_text_len = new_text.len();
 256        let icon_path = uri.icon_path(cx);
 257        Some(Completion {
 258            replace_range: source_range.clone(),
 259            new_text,
 260            label,
 261            documentation: None,
 262            source: project::CompletionSource::Custom,
 263            icon_path: Some(icon_path),
 264            insert_text_mode: None,
 265            confirm: Some(confirm_completion_callback(
 266                symbol.name.into(),
 267                source_range.start,
 268                new_text_len - 1,
 269                message_editor,
 270                uri,
 271            )),
 272        })
 273    }
 274
 275    fn completion_for_fetch(
 276        source_range: Range<Anchor>,
 277        url_to_fetch: SharedString,
 278        message_editor: WeakEntity<MessageEditor>,
 279        cx: &mut App,
 280    ) -> Option<Completion> {
 281        let new_text = format!("@fetch {} ", url_to_fetch);
 282        let url_to_fetch = url::Url::parse(url_to_fetch.as_ref())
 283            .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}")))
 284            .ok()?;
 285        let mention_uri = MentionUri::Fetch {
 286            url: url_to_fetch.clone(),
 287        };
 288        let icon_path = mention_uri.icon_path(cx);
 289        Some(Completion {
 290            replace_range: source_range.clone(),
 291            new_text: new_text.clone(),
 292            label: CodeLabel::plain(url_to_fetch.to_string(), None),
 293            documentation: None,
 294            source: project::CompletionSource::Custom,
 295            icon_path: Some(icon_path),
 296            insert_text_mode: None,
 297            confirm: Some(confirm_completion_callback(
 298                url_to_fetch.to_string().into(),
 299                source_range.start,
 300                new_text.len() - 1,
 301                message_editor,
 302                mention_uri,
 303            )),
 304        })
 305    }
 306
 307    pub(crate) fn completion_for_action(
 308        action: ContextPickerAction,
 309        source_range: Range<Anchor>,
 310        message_editor: WeakEntity<MessageEditor>,
 311        workspace: &Entity<Workspace>,
 312        cx: &mut App,
 313    ) -> Option<Completion> {
 314        let (new_text, on_action) = match action {
 315            ContextPickerAction::AddSelections => {
 316                const PLACEHOLDER: &str = "selection ";
 317                let selections = selection_ranges(workspace, cx)
 318                    .into_iter()
 319                    .enumerate()
 320                    .map(|(ix, (buffer, range))| {
 321                        (
 322                            buffer,
 323                            range,
 324                            (PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1),
 325                        )
 326                    })
 327                    .collect::<Vec<_>>();
 328
 329                let new_text: String = PLACEHOLDER.repeat(selections.len());
 330
 331                let callback = Arc::new({
 332                    let source_range = source_range.clone();
 333                    move |_, window: &mut Window, cx: &mut App| {
 334                        let selections = selections.clone();
 335                        let message_editor = message_editor.clone();
 336                        let source_range = source_range.clone();
 337                        window.defer(cx, move |window, cx| {
 338                            message_editor
 339                                .update(cx, |message_editor, cx| {
 340                                    message_editor.confirm_mention_for_selection(
 341                                        source_range,
 342                                        selections,
 343                                        window,
 344                                        cx,
 345                                    )
 346                                })
 347                                .ok();
 348                        });
 349                        false
 350                    }
 351                });
 352
 353                (new_text, callback)
 354            }
 355        };
 356
 357        Some(Completion {
 358            replace_range: source_range,
 359            new_text,
 360            label: CodeLabel::plain(action.label().to_string(), None),
 361            icon_path: Some(action.icon().path().into()),
 362            documentation: None,
 363            source: project::CompletionSource::Custom,
 364            insert_text_mode: None,
 365            // This ensures that when a user accepts this completion, the
 366            // completion menu will still be shown after "@category " is
 367            // inserted
 368            confirm: Some(on_action),
 369        })
 370    }
 371
 372    fn search(
 373        &self,
 374        mode: Option<ContextPickerMode>,
 375        query: String,
 376        cancellation_flag: Arc<AtomicBool>,
 377        cx: &mut App,
 378    ) -> Task<Vec<Match>> {
 379        let Some(workspace) = self.workspace.upgrade() else {
 380            return Task::ready(Vec::default());
 381        };
 382        match mode {
 383            Some(ContextPickerMode::File) => {
 384                let search_files_task = search_files(query, cancellation_flag, &workspace, cx);
 385                cx.background_spawn(async move {
 386                    search_files_task
 387                        .await
 388                        .into_iter()
 389                        .map(Match::File)
 390                        .collect()
 391                })
 392            }
 393
 394            Some(ContextPickerMode::Symbol) => {
 395                let search_symbols_task = search_symbols(query, cancellation_flag, &workspace, cx);
 396                cx.background_spawn(async move {
 397                    search_symbols_task
 398                        .await
 399                        .into_iter()
 400                        .map(Match::Symbol)
 401                        .collect()
 402                })
 403            }
 404
 405            Some(ContextPickerMode::Thread) => {
 406                let search_threads_task =
 407                    search_threads(query, cancellation_flag, &self.history_store, cx);
 408                cx.background_spawn(async move {
 409                    search_threads_task
 410                        .await
 411                        .into_iter()
 412                        .map(Match::Thread)
 413                        .collect()
 414                })
 415            }
 416
 417            Some(ContextPickerMode::Fetch) => {
 418                if !query.is_empty() {
 419                    Task::ready(vec![Match::Fetch(query.into())])
 420                } else {
 421                    Task::ready(Vec::new())
 422                }
 423            }
 424
 425            Some(ContextPickerMode::Rules) => {
 426                if let Some(prompt_store) = self.prompt_store.as_ref() {
 427                    let search_rules_task =
 428                        search_rules(query, cancellation_flag, prompt_store, cx);
 429                    cx.background_spawn(async move {
 430                        search_rules_task
 431                            .await
 432                            .into_iter()
 433                            .map(Match::Rules)
 434                            .collect::<Vec<_>>()
 435                    })
 436                } else {
 437                    Task::ready(Vec::new())
 438                }
 439            }
 440
 441            None if query.is_empty() => {
 442                let mut matches = self.recent_context_picker_entries(&workspace, cx);
 443
 444                matches.extend(
 445                    self.available_context_picker_entries(&workspace, cx)
 446                        .into_iter()
 447                        .map(|mode| {
 448                            Match::Entry(EntryMatch {
 449                                entry: mode,
 450                                mat: None,
 451                            })
 452                        }),
 453                );
 454
 455                Task::ready(matches)
 456            }
 457            None => {
 458                let executor = cx.background_executor().clone();
 459
 460                let search_files_task =
 461                    search_files(query.clone(), cancellation_flag, &workspace, cx);
 462
 463                let entries = self.available_context_picker_entries(&workspace, cx);
 464                let entry_candidates = entries
 465                    .iter()
 466                    .enumerate()
 467                    .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword()))
 468                    .collect::<Vec<_>>();
 469
 470                cx.background_spawn(async move {
 471                    let mut matches = search_files_task
 472                        .await
 473                        .into_iter()
 474                        .map(Match::File)
 475                        .collect::<Vec<_>>();
 476
 477                    let entry_matches = fuzzy::match_strings(
 478                        &entry_candidates,
 479                        &query,
 480                        false,
 481                        true,
 482                        100,
 483                        &Arc::new(AtomicBool::default()),
 484                        executor,
 485                    )
 486                    .await;
 487
 488                    matches.extend(entry_matches.into_iter().map(|mat| {
 489                        Match::Entry(EntryMatch {
 490                            entry: entries[mat.candidate_id],
 491                            mat: Some(mat),
 492                        })
 493                    }));
 494
 495                    matches.sort_by(|a, b| {
 496                        b.score()
 497                            .partial_cmp(&a.score())
 498                            .unwrap_or(std::cmp::Ordering::Equal)
 499                    });
 500
 501                    matches
 502                })
 503            }
 504        }
 505    }
 506
 507    fn recent_context_picker_entries(
 508        &self,
 509        workspace: &Entity<Workspace>,
 510        cx: &mut App,
 511    ) -> Vec<Match> {
 512        let mut recent = Vec::with_capacity(6);
 513
 514        let mut mentions = self
 515            .message_editor
 516            .read_with(cx, |message_editor, _cx| message_editor.mentions())
 517            .unwrap_or_default();
 518        let workspace = workspace.read(cx);
 519        let project = workspace.project().read(cx);
 520
 521        if let Some(agent_panel) = workspace.panel::<AgentPanel>(cx)
 522            && let Some(thread) = agent_panel.read(cx).active_agent_thread(cx)
 523        {
 524            let thread = thread.read(cx);
 525            mentions.insert(MentionUri::Thread {
 526                id: thread.session_id().clone(),
 527                name: thread.title().into(),
 528            });
 529        }
 530
 531        recent.extend(
 532            workspace
 533                .recent_navigation_history_iter(cx)
 534                .filter(|(_, abs_path)| {
 535                    abs_path.as_ref().is_none_or(|path| {
 536                        !mentions.contains(&MentionUri::File {
 537                            abs_path: path.clone(),
 538                        })
 539                    })
 540                })
 541                .take(4)
 542                .filter_map(|(project_path, _)| {
 543                    project
 544                        .worktree_for_id(project_path.worktree_id, cx)
 545                        .map(|worktree| {
 546                            let path_prefix = worktree.read(cx).root_name().into();
 547                            Match::File(FileMatch {
 548                                mat: fuzzy::PathMatch {
 549                                    score: 1.,
 550                                    positions: Vec::new(),
 551                                    worktree_id: project_path.worktree_id.to_usize(),
 552                                    path: project_path.path,
 553                                    path_prefix,
 554                                    is_dir: false,
 555                                    distance_to_relative_ancestor: 0,
 556                                },
 557                                is_recent: true,
 558                            })
 559                        })
 560                }),
 561        );
 562
 563        if self.prompt_capabilities.get().embedded_context {
 564            const RECENT_COUNT: usize = 2;
 565            let threads = self
 566                .history_store
 567                .read(cx)
 568                .recently_opened_entries(cx)
 569                .into_iter()
 570                .filter(|thread| !mentions.contains(&thread.mention_uri()))
 571                .take(RECENT_COUNT)
 572                .collect::<Vec<_>>();
 573
 574            recent.extend(threads.into_iter().map(Match::RecentThread));
 575        }
 576
 577        recent
 578    }
 579
 580    fn available_context_picker_entries(
 581        &self,
 582        workspace: &Entity<Workspace>,
 583        cx: &mut App,
 584    ) -> Vec<ContextPickerEntry> {
 585        let embedded_context = self.prompt_capabilities.get().embedded_context;
 586        let mut entries = if embedded_context {
 587            vec![
 588                ContextPickerEntry::Mode(ContextPickerMode::File),
 589                ContextPickerEntry::Mode(ContextPickerMode::Symbol),
 590                ContextPickerEntry::Mode(ContextPickerMode::Thread),
 591            ]
 592        } else {
 593            // File is always available, but we don't need a mode entry
 594            vec![]
 595        };
 596
 597        let has_selection = workspace
 598            .read(cx)
 599            .active_item(cx)
 600            .and_then(|item| item.downcast::<Editor>())
 601            .is_some_and(|editor| {
 602                editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))
 603            });
 604        if has_selection {
 605            entries.push(ContextPickerEntry::Action(
 606                ContextPickerAction::AddSelections,
 607            ));
 608        }
 609
 610        if embedded_context {
 611            if self.prompt_store.is_some() {
 612                entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
 613            }
 614
 615            entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
 616        }
 617
 618        entries
 619    }
 620}
 621
 622fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
 623    let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
 624    let mut label = CodeLabel::default();
 625
 626    label.push_str(file_name, None);
 627    label.push_str(" ", None);
 628
 629    if let Some(directory) = directory {
 630        label.push_str(directory, comment_id);
 631    }
 632
 633    label.filter_range = 0..label.text().len();
 634
 635    label
 636}
 637
 638impl CompletionProvider for ContextPickerCompletionProvider {
 639    fn completions(
 640        &self,
 641        _excerpt_id: ExcerptId,
 642        buffer: &Entity<Buffer>,
 643        buffer_position: Anchor,
 644        _trigger: CompletionContext,
 645        _window: &mut Window,
 646        cx: &mut Context<Editor>,
 647    ) -> Task<Result<Vec<CompletionResponse>>> {
 648        let state = buffer.update(cx, |buffer, _cx| {
 649            let position = buffer_position.to_point(buffer);
 650            let line_start = Point::new(position.row, 0);
 651            let offset_to_line = buffer.point_to_offset(line_start);
 652            let mut lines = buffer.text_for_range(line_start..position).lines();
 653            let line = lines.next()?;
 654            MentionCompletion::try_parse(
 655                self.prompt_capabilities.get().embedded_context,
 656                line,
 657                offset_to_line,
 658            )
 659        });
 660        let Some(state) = state else {
 661            return Task::ready(Ok(Vec::new()));
 662        };
 663
 664        let Some(workspace) = self.workspace.upgrade() else {
 665            return Task::ready(Ok(Vec::new()));
 666        };
 667
 668        let project = workspace.read(cx).project().clone();
 669        let snapshot = buffer.read(cx).snapshot();
 670        let source_range = snapshot.anchor_before(state.source_range.start)
 671            ..snapshot.anchor_after(state.source_range.end);
 672
 673        let editor = self.message_editor.clone();
 674
 675        let MentionCompletion { mode, argument, .. } = state;
 676        let query = argument.unwrap_or_else(|| "".to_string());
 677
 678        let search_task = self.search(mode, query, Arc::<AtomicBool>::default(), cx);
 679
 680        cx.spawn(async move |_, cx| {
 681            let matches = search_task.await;
 682
 683            let completions = cx.update(|cx| {
 684                matches
 685                    .into_iter()
 686                    .filter_map(|mat| match mat {
 687                        Match::File(FileMatch { mat, is_recent }) => {
 688                            let project_path = ProjectPath {
 689                                worktree_id: WorktreeId::from_usize(mat.worktree_id),
 690                                path: mat.path.clone(),
 691                            };
 692
 693                            Self::completion_for_path(
 694                                project_path,
 695                                &mat.path_prefix,
 696                                is_recent,
 697                                mat.is_dir,
 698                                source_range.clone(),
 699                                editor.clone(),
 700                                project.clone(),
 701                                cx,
 702                            )
 703                        }
 704
 705                        Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
 706                            symbol,
 707                            source_range.clone(),
 708                            editor.clone(),
 709                            workspace.clone(),
 710                            cx,
 711                        ),
 712
 713                        Match::Thread(thread) => Some(Self::completion_for_thread(
 714                            thread,
 715                            source_range.clone(),
 716                            false,
 717                            editor.clone(),
 718                            cx,
 719                        )),
 720
 721                        Match::RecentThread(thread) => Some(Self::completion_for_thread(
 722                            thread,
 723                            source_range.clone(),
 724                            true,
 725                            editor.clone(),
 726                            cx,
 727                        )),
 728
 729                        Match::Rules(user_rules) => Some(Self::completion_for_rules(
 730                            user_rules,
 731                            source_range.clone(),
 732                            editor.clone(),
 733                            cx,
 734                        )),
 735
 736                        Match::Fetch(url) => Self::completion_for_fetch(
 737                            source_range.clone(),
 738                            url,
 739                            editor.clone(),
 740                            cx,
 741                        ),
 742
 743                        Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
 744                            entry,
 745                            source_range.clone(),
 746                            editor.clone(),
 747                            &workspace,
 748                            cx,
 749                        ),
 750                    })
 751                    .collect()
 752            })?;
 753
 754            Ok(vec![CompletionResponse {
 755                completions,
 756                // Since this does its own filtering (see `filter_completions()` returns false),
 757                // there is no benefit to computing whether this set of completions is incomplete.
 758                is_incomplete: true,
 759            }])
 760        })
 761    }
 762
 763    fn is_completion_trigger(
 764        &self,
 765        buffer: &Entity<language::Buffer>,
 766        position: language::Anchor,
 767        _text: &str,
 768        _trigger_in_words: bool,
 769        _menu_is_open: bool,
 770        cx: &mut Context<Editor>,
 771    ) -> bool {
 772        let buffer = buffer.read(cx);
 773        let position = position.to_point(buffer);
 774        let line_start = Point::new(position.row, 0);
 775        let offset_to_line = buffer.point_to_offset(line_start);
 776        let mut lines = buffer.text_for_range(line_start..position).lines();
 777        if let Some(line) = lines.next() {
 778            MentionCompletion::try_parse(
 779                self.prompt_capabilities.get().embedded_context,
 780                line,
 781                offset_to_line,
 782            )
 783            .map(|completion| {
 784                completion.source_range.start <= offset_to_line + position.column as usize
 785                    && completion.source_range.end >= offset_to_line + position.column as usize
 786            })
 787            .unwrap_or(false)
 788        } else {
 789            false
 790        }
 791    }
 792
 793    fn sort_completions(&self) -> bool {
 794        false
 795    }
 796
 797    fn filter_completions(&self) -> bool {
 798        false
 799    }
 800}
 801
 802pub(crate) fn search_threads(
 803    query: String,
 804    cancellation_flag: Arc<AtomicBool>,
 805    history_store: &Entity<HistoryStore>,
 806    cx: &mut App,
 807) -> Task<Vec<HistoryEntry>> {
 808    let threads = history_store.read(cx).entries().collect();
 809    if query.is_empty() {
 810        return Task::ready(threads);
 811    }
 812
 813    let executor = cx.background_executor().clone();
 814    cx.background_spawn(async move {
 815        let candidates = threads
 816            .iter()
 817            .enumerate()
 818            .map(|(id, thread)| StringMatchCandidate::new(id, thread.title()))
 819            .collect::<Vec<_>>();
 820        let matches = fuzzy::match_strings(
 821            &candidates,
 822            &query,
 823            false,
 824            true,
 825            100,
 826            &cancellation_flag,
 827            executor,
 828        )
 829        .await;
 830
 831        matches
 832            .into_iter()
 833            .map(|mat| threads[mat.candidate_id].clone())
 834            .collect()
 835    })
 836}
 837
 838fn confirm_completion_callback(
 839    crease_text: SharedString,
 840    start: Anchor,
 841    content_len: usize,
 842    message_editor: WeakEntity<MessageEditor>,
 843    mention_uri: MentionUri,
 844) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
 845    Arc::new(move |_, window, cx| {
 846        let message_editor = message_editor.clone();
 847        let crease_text = crease_text.clone();
 848        let mention_uri = mention_uri.clone();
 849        window.defer(cx, move |window, cx| {
 850            message_editor
 851                .clone()
 852                .update(cx, |message_editor, cx| {
 853                    message_editor
 854                        .confirm_completion(
 855                            crease_text,
 856                            start,
 857                            content_len,
 858                            mention_uri,
 859                            window,
 860                            cx,
 861                        )
 862                        .detach();
 863                })
 864                .ok();
 865        });
 866        false
 867    })
 868}
 869
 870#[derive(Debug, Default, PartialEq)]
 871struct MentionCompletion {
 872    source_range: Range<usize>,
 873    mode: Option<ContextPickerMode>,
 874    argument: Option<String>,
 875}
 876
 877impl MentionCompletion {
 878    fn try_parse(allow_non_file_mentions: bool, line: &str, offset_to_line: usize) -> Option<Self> {
 879        let last_mention_start = line.rfind('@')?;
 880        if last_mention_start >= line.len() {
 881            return Some(Self::default());
 882        }
 883        if last_mention_start > 0
 884            && line
 885                .chars()
 886                .nth(last_mention_start - 1)
 887                .is_some_and(|c| !c.is_whitespace())
 888        {
 889            return None;
 890        }
 891
 892        let rest_of_line = &line[last_mention_start + 1..];
 893
 894        let mut mode = None;
 895        let mut argument = None;
 896
 897        let mut parts = rest_of_line.split_whitespace();
 898        let mut end = last_mention_start + 1;
 899        if let Some(mode_text) = parts.next() {
 900            end += mode_text.len();
 901
 902            if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok()
 903                && (allow_non_file_mentions || matches!(parsed_mode, ContextPickerMode::File))
 904            {
 905                mode = Some(parsed_mode);
 906            } else {
 907                argument = Some(mode_text.to_string());
 908            }
 909            match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
 910                Some(whitespace_count) => {
 911                    if let Some(argument_text) = parts.next() {
 912                        argument = Some(argument_text.to_string());
 913                        end += whitespace_count + argument_text.len();
 914                    }
 915                }
 916                None => {
 917                    // Rest of line is entirely whitespace
 918                    end += rest_of_line.len() - mode_text.len();
 919                }
 920            }
 921        }
 922
 923        Some(Self {
 924            source_range: last_mention_start + offset_to_line..end + offset_to_line,
 925            mode,
 926            argument,
 927        })
 928    }
 929}
 930
 931#[cfg(test)]
 932mod tests {
 933    use super::*;
 934
 935    #[test]
 936    fn test_mention_completion_parse() {
 937        assert_eq!(MentionCompletion::try_parse(true, "Lorem Ipsum", 0), None);
 938
 939        assert_eq!(
 940            MentionCompletion::try_parse(true, "Lorem @", 0),
 941            Some(MentionCompletion {
 942                source_range: 6..7,
 943                mode: None,
 944                argument: None,
 945            })
 946        );
 947
 948        assert_eq!(
 949            MentionCompletion::try_parse(true, "Lorem @file", 0),
 950            Some(MentionCompletion {
 951                source_range: 6..11,
 952                mode: Some(ContextPickerMode::File),
 953                argument: None,
 954            })
 955        );
 956
 957        assert_eq!(
 958            MentionCompletion::try_parse(true, "Lorem @file ", 0),
 959            Some(MentionCompletion {
 960                source_range: 6..12,
 961                mode: Some(ContextPickerMode::File),
 962                argument: None,
 963            })
 964        );
 965
 966        assert_eq!(
 967            MentionCompletion::try_parse(true, "Lorem @file main.rs", 0),
 968            Some(MentionCompletion {
 969                source_range: 6..19,
 970                mode: Some(ContextPickerMode::File),
 971                argument: Some("main.rs".to_string()),
 972            })
 973        );
 974
 975        assert_eq!(
 976            MentionCompletion::try_parse(true, "Lorem @file main.rs ", 0),
 977            Some(MentionCompletion {
 978                source_range: 6..19,
 979                mode: Some(ContextPickerMode::File),
 980                argument: Some("main.rs".to_string()),
 981            })
 982        );
 983
 984        assert_eq!(
 985            MentionCompletion::try_parse(true, "Lorem @file main.rs Ipsum", 0),
 986            Some(MentionCompletion {
 987                source_range: 6..19,
 988                mode: Some(ContextPickerMode::File),
 989                argument: Some("main.rs".to_string()),
 990            })
 991        );
 992
 993        assert_eq!(
 994            MentionCompletion::try_parse(true, "Lorem @main", 0),
 995            Some(MentionCompletion {
 996                source_range: 6..11,
 997                mode: None,
 998                argument: Some("main".to_string()),
 999            })
1000        );
1001
1002        assert_eq!(MentionCompletion::try_parse(true, "test@", 0), None);
1003
1004        // Allowed non-file mentions
1005
1006        assert_eq!(
1007            MentionCompletion::try_parse(true, "Lorem @symbol main", 0),
1008            Some(MentionCompletion {
1009                source_range: 6..18,
1010                mode: Some(ContextPickerMode::Symbol),
1011                argument: Some("main".to_string()),
1012            })
1013        );
1014
1015        // Disallowed non-file mentions
1016
1017        assert_eq!(
1018            MentionCompletion::try_parse(false, "Lorem @symbol main", 0),
1019            Some(MentionCompletion {
1020                source_range: 6..18,
1021                mode: None,
1022                argument: Some("main".to_string()),
1023            })
1024        );
1025    }
1026}