completion_provider.rs

   1use std::cell::{Cell, RefCell};
   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::lsp_store::CompletionDocumentation;
  17use project::{
  18    Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId,
  19};
  20use prompt_store::PromptStore;
  21use rope::Point;
  22use text::{Anchor, ToPoint as _};
  23use ui::prelude::*;
  24use workspace::Workspace;
  25
  26use crate::AgentPanel;
  27use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
  28use crate::context_picker::file_context_picker::{FileMatch, search_files};
  29use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules};
  30use crate::context_picker::symbol_context_picker::SymbolMatch;
  31use crate::context_picker::symbol_context_picker::search_symbols;
  32use crate::context_picker::{
  33    ContextPickerAction, ContextPickerEntry, ContextPickerMode, selection_ranges,
  34};
  35
  36pub(crate) enum Match {
  37    File(FileMatch),
  38    Symbol(SymbolMatch),
  39    Thread(HistoryEntry),
  40    RecentThread(HistoryEntry),
  41    Fetch(SharedString),
  42    Rules(RulesContextEntry),
  43    Entry(EntryMatch),
  44}
  45
  46pub struct EntryMatch {
  47    mat: Option<StringMatch>,
  48    entry: ContextPickerEntry,
  49}
  50
  51impl Match {
  52    pub fn score(&self) -> f64 {
  53        match self {
  54            Match::File(file) => file.mat.score,
  55            Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
  56            Match::Thread(_) => 1.,
  57            Match::RecentThread(_) => 1.,
  58            Match::Symbol(_) => 1.,
  59            Match::Rules(_) => 1.,
  60            Match::Fetch(_) => 1.,
  61        }
  62    }
  63}
  64
  65pub struct ContextPickerCompletionProvider {
  66    message_editor: WeakEntity<MessageEditor>,
  67    workspace: WeakEntity<Workspace>,
  68    history_store: Entity<HistoryStore>,
  69    prompt_store: Option<Entity<PromptStore>>,
  70    prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
  71    available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
  72}
  73
  74impl ContextPickerCompletionProvider {
  75    pub fn new(
  76        message_editor: WeakEntity<MessageEditor>,
  77        workspace: WeakEntity<Workspace>,
  78        history_store: Entity<HistoryStore>,
  79        prompt_store: Option<Entity<PromptStore>>,
  80        prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
  81        available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
  82    ) -> Self {
  83        Self {
  84            message_editor,
  85            workspace,
  86            history_store,
  87            prompt_store,
  88            prompt_capabilities,
  89            available_commands,
  90        }
  91    }
  92
  93    fn completion_for_entry(
  94        entry: ContextPickerEntry,
  95        source_range: Range<Anchor>,
  96        message_editor: WeakEntity<MessageEditor>,
  97        workspace: &Entity<Workspace>,
  98        cx: &mut App,
  99    ) -> Option<Completion> {
 100        match entry {
 101            ContextPickerEntry::Mode(mode) => Some(Completion {
 102                replace_range: source_range,
 103                new_text: format!("@{} ", mode.keyword()),
 104                label: CodeLabel::plain(mode.label().to_string(), None),
 105                icon_path: Some(mode.icon().path().into()),
 106                documentation: None,
 107                source: project::CompletionSource::Custom,
 108                insert_text_mode: None,
 109                // This ensures that when a user accepts this completion, the
 110                // completion menu will still be shown after "@category " is
 111                // inserted
 112                confirm: Some(Arc::new(|_, _, _| true)),
 113            }),
 114            ContextPickerEntry::Action(action) => {
 115                Self::completion_for_action(action, source_range, message_editor, workspace, cx)
 116            }
 117        }
 118    }
 119
 120    fn completion_for_thread(
 121        thread_entry: HistoryEntry,
 122        source_range: Range<Anchor>,
 123        recent: bool,
 124        editor: WeakEntity<MessageEditor>,
 125        cx: &mut App,
 126    ) -> Completion {
 127        let uri = thread_entry.mention_uri();
 128
 129        let icon_for_completion = if recent {
 130            IconName::HistoryRerun.path().into()
 131        } else {
 132            uri.icon_path(cx)
 133        };
 134
 135        let new_text = format!("{} ", uri.as_link());
 136
 137        let new_text_len = new_text.len();
 138        Completion {
 139            replace_range: source_range.clone(),
 140            new_text,
 141            label: CodeLabel::plain(thread_entry.title().to_string(), None),
 142            documentation: None,
 143            insert_text_mode: None,
 144            source: project::CompletionSource::Custom,
 145            icon_path: Some(icon_for_completion),
 146            confirm: Some(confirm_completion_callback(
 147                thread_entry.title().clone(),
 148                source_range.start,
 149                new_text_len - 1,
 150                editor,
 151                uri,
 152            )),
 153        }
 154    }
 155
 156    fn completion_for_rules(
 157        rule: RulesContextEntry,
 158        source_range: Range<Anchor>,
 159        editor: WeakEntity<MessageEditor>,
 160        cx: &mut App,
 161    ) -> Completion {
 162        let uri = MentionUri::Rule {
 163            id: rule.prompt_id.into(),
 164            name: rule.title.to_string(),
 165        };
 166        let new_text = format!("{} ", uri.as_link());
 167        let new_text_len = new_text.len();
 168        let icon_path = uri.icon_path(cx);
 169        Completion {
 170            replace_range: source_range.clone(),
 171            new_text,
 172            label: CodeLabel::plain(rule.title.to_string(), None),
 173            documentation: None,
 174            insert_text_mode: None,
 175            source: project::CompletionSource::Custom,
 176            icon_path: Some(icon_path),
 177            confirm: Some(confirm_completion_callback(
 178                rule.title,
 179                source_range.start,
 180                new_text_len - 1,
 181                editor,
 182                uri,
 183            )),
 184        }
 185    }
 186
 187    pub(crate) fn completion_for_path(
 188        project_path: ProjectPath,
 189        path_prefix: &str,
 190        is_recent: bool,
 191        is_directory: bool,
 192        source_range: Range<Anchor>,
 193        message_editor: WeakEntity<MessageEditor>,
 194        project: Entity<Project>,
 195        cx: &mut App,
 196    ) -> Option<Completion> {
 197        let (file_name, directory) =
 198            crate::context_picker::file_context_picker::extract_file_name_and_directory(
 199                &project_path.path,
 200                path_prefix,
 201            );
 202
 203        let label =
 204            build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
 205
 206        let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
 207
 208        let uri = if is_directory {
 209            MentionUri::Directory { abs_path }
 210        } else {
 211            MentionUri::File { abs_path }
 212        };
 213
 214        let crease_icon_path = uri.icon_path(cx);
 215        let completion_icon_path = if is_recent {
 216            IconName::HistoryRerun.path().into()
 217        } else {
 218            crease_icon_path
 219        };
 220
 221        let new_text = format!("{} ", uri.as_link());
 222        let new_text_len = new_text.len();
 223        Some(Completion {
 224            replace_range: source_range.clone(),
 225            new_text,
 226            label,
 227            documentation: None,
 228            source: project::CompletionSource::Custom,
 229            icon_path: Some(completion_icon_path),
 230            insert_text_mode: None,
 231            confirm: Some(confirm_completion_callback(
 232                file_name,
 233                source_range.start,
 234                new_text_len - 1,
 235                message_editor,
 236                uri,
 237            )),
 238        })
 239    }
 240
 241    fn completion_for_symbol(
 242        symbol: Symbol,
 243        source_range: Range<Anchor>,
 244        message_editor: WeakEntity<MessageEditor>,
 245        workspace: Entity<Workspace>,
 246        cx: &mut App,
 247    ) -> Option<Completion> {
 248        let project = workspace.read(cx).project().clone();
 249
 250        let label = CodeLabel::plain(symbol.name.clone(), None);
 251
 252        let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?;
 253        let uri = MentionUri::Symbol {
 254            abs_path,
 255            name: symbol.name.clone(),
 256            line_range: symbol.range.start.0.row..=symbol.range.end.0.row,
 257        };
 258        let new_text = format!("{} ", uri.as_link());
 259        let new_text_len = new_text.len();
 260        let icon_path = uri.icon_path(cx);
 261        Some(Completion {
 262            replace_range: source_range.clone(),
 263            new_text,
 264            label,
 265            documentation: None,
 266            source: project::CompletionSource::Custom,
 267            icon_path: Some(icon_path),
 268            insert_text_mode: None,
 269            confirm: Some(confirm_completion_callback(
 270                symbol.name.into(),
 271                source_range.start,
 272                new_text_len - 1,
 273                message_editor,
 274                uri,
 275            )),
 276        })
 277    }
 278
 279    fn completion_for_fetch(
 280        source_range: Range<Anchor>,
 281        url_to_fetch: SharedString,
 282        message_editor: WeakEntity<MessageEditor>,
 283        cx: &mut App,
 284    ) -> Option<Completion> {
 285        let new_text = format!("@fetch {} ", url_to_fetch);
 286        let url_to_fetch = url::Url::parse(url_to_fetch.as_ref())
 287            .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}")))
 288            .ok()?;
 289        let mention_uri = MentionUri::Fetch {
 290            url: url_to_fetch.clone(),
 291        };
 292        let icon_path = mention_uri.icon_path(cx);
 293        Some(Completion {
 294            replace_range: source_range.clone(),
 295            new_text: new_text.clone(),
 296            label: CodeLabel::plain(url_to_fetch.to_string(), None),
 297            documentation: None,
 298            source: project::CompletionSource::Custom,
 299            icon_path: Some(icon_path),
 300            insert_text_mode: None,
 301            confirm: Some(confirm_completion_callback(
 302                url_to_fetch.to_string().into(),
 303                source_range.start,
 304                new_text.len() - 1,
 305                message_editor,
 306                mention_uri,
 307            )),
 308        })
 309    }
 310
 311    pub(crate) fn completion_for_action(
 312        action: ContextPickerAction,
 313        source_range: Range<Anchor>,
 314        message_editor: WeakEntity<MessageEditor>,
 315        workspace: &Entity<Workspace>,
 316        cx: &mut App,
 317    ) -> Option<Completion> {
 318        let (new_text, on_action) = match action {
 319            ContextPickerAction::AddSelections => {
 320                const PLACEHOLDER: &str = "selection ";
 321                let selections = selection_ranges(workspace, cx)
 322                    .into_iter()
 323                    .enumerate()
 324                    .map(|(ix, (buffer, range))| {
 325                        (
 326                            buffer,
 327                            range,
 328                            (PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1),
 329                        )
 330                    })
 331                    .collect::<Vec<_>>();
 332
 333                let new_text: String = PLACEHOLDER.repeat(selections.len());
 334
 335                let callback = Arc::new({
 336                    let source_range = source_range.clone();
 337                    move |_, window: &mut Window, cx: &mut App| {
 338                        let selections = selections.clone();
 339                        let message_editor = message_editor.clone();
 340                        let source_range = source_range.clone();
 341                        window.defer(cx, move |window, cx| {
 342                            message_editor
 343                                .update(cx, |message_editor, cx| {
 344                                    message_editor.confirm_mention_for_selection(
 345                                        source_range,
 346                                        selections,
 347                                        window,
 348                                        cx,
 349                                    )
 350                                })
 351                                .ok();
 352                        });
 353                        false
 354                    }
 355                });
 356
 357                (new_text, callback)
 358            }
 359        };
 360
 361        Some(Completion {
 362            replace_range: source_range,
 363            new_text,
 364            label: CodeLabel::plain(action.label().to_string(), None),
 365            icon_path: Some(action.icon().path().into()),
 366            documentation: None,
 367            source: project::CompletionSource::Custom,
 368            insert_text_mode: None,
 369            // This ensures that when a user accepts this completion, the
 370            // completion menu will still be shown after "@category " is
 371            // inserted
 372            confirm: Some(on_action),
 373        })
 374    }
 375
 376    fn search_slash_commands(
 377        &self,
 378        query: String,
 379        cx: &mut App,
 380    ) -> Task<Vec<acp::AvailableCommand>> {
 381        let commands = self.available_commands.borrow().clone();
 382        if commands.is_empty() {
 383            return Task::ready(Vec::new());
 384        }
 385
 386        cx.spawn(async move |cx| {
 387            let candidates = commands
 388                .iter()
 389                .enumerate()
 390                .map(|(id, command)| StringMatchCandidate::new(id, &command.name))
 391                .collect::<Vec<_>>();
 392
 393            let matches = fuzzy::match_strings(
 394                &candidates,
 395                &query,
 396                false,
 397                true,
 398                100,
 399                &Arc::new(AtomicBool::default()),
 400                cx.background_executor().clone(),
 401            )
 402            .await;
 403
 404            matches
 405                .into_iter()
 406                .map(|mat| commands[mat.candidate_id].clone())
 407                .collect()
 408        })
 409    }
 410
 411    fn search_mentions(
 412        &self,
 413        mode: Option<ContextPickerMode>,
 414        query: String,
 415        cancellation_flag: Arc<AtomicBool>,
 416        cx: &mut App,
 417    ) -> Task<Vec<Match>> {
 418        let Some(workspace) = self.workspace.upgrade() else {
 419            return Task::ready(Vec::default());
 420        };
 421        match mode {
 422            Some(ContextPickerMode::File) => {
 423                let search_files_task = search_files(query, cancellation_flag, &workspace, cx);
 424                cx.background_spawn(async move {
 425                    search_files_task
 426                        .await
 427                        .into_iter()
 428                        .map(Match::File)
 429                        .collect()
 430                })
 431            }
 432
 433            Some(ContextPickerMode::Symbol) => {
 434                let search_symbols_task = search_symbols(query, cancellation_flag, &workspace, cx);
 435                cx.background_spawn(async move {
 436                    search_symbols_task
 437                        .await
 438                        .into_iter()
 439                        .map(Match::Symbol)
 440                        .collect()
 441                })
 442            }
 443
 444            Some(ContextPickerMode::Thread) => {
 445                let search_threads_task =
 446                    search_threads(query, cancellation_flag, &self.history_store, cx);
 447                cx.background_spawn(async move {
 448                    search_threads_task
 449                        .await
 450                        .into_iter()
 451                        .map(Match::Thread)
 452                        .collect()
 453                })
 454            }
 455
 456            Some(ContextPickerMode::Fetch) => {
 457                if !query.is_empty() {
 458                    Task::ready(vec![Match::Fetch(query.into())])
 459                } else {
 460                    Task::ready(Vec::new())
 461                }
 462            }
 463
 464            Some(ContextPickerMode::Rules) => {
 465                if let Some(prompt_store) = self.prompt_store.as_ref() {
 466                    let search_rules_task =
 467                        search_rules(query, cancellation_flag, prompt_store, cx);
 468                    cx.background_spawn(async move {
 469                        search_rules_task
 470                            .await
 471                            .into_iter()
 472                            .map(Match::Rules)
 473                            .collect::<Vec<_>>()
 474                    })
 475                } else {
 476                    Task::ready(Vec::new())
 477                }
 478            }
 479
 480            None if query.is_empty() => {
 481                let mut matches = self.recent_context_picker_entries(&workspace, cx);
 482
 483                matches.extend(
 484                    self.available_context_picker_entries(&workspace, cx)
 485                        .into_iter()
 486                        .map(|mode| {
 487                            Match::Entry(EntryMatch {
 488                                entry: mode,
 489                                mat: None,
 490                            })
 491                        }),
 492                );
 493
 494                Task::ready(matches)
 495            }
 496            None => {
 497                let executor = cx.background_executor().clone();
 498
 499                let search_files_task =
 500                    search_files(query.clone(), cancellation_flag, &workspace, cx);
 501
 502                let entries = self.available_context_picker_entries(&workspace, cx);
 503                let entry_candidates = entries
 504                    .iter()
 505                    .enumerate()
 506                    .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword()))
 507                    .collect::<Vec<_>>();
 508
 509                cx.background_spawn(async move {
 510                    let mut matches = search_files_task
 511                        .await
 512                        .into_iter()
 513                        .map(Match::File)
 514                        .collect::<Vec<_>>();
 515
 516                    let entry_matches = fuzzy::match_strings(
 517                        &entry_candidates,
 518                        &query,
 519                        false,
 520                        true,
 521                        100,
 522                        &Arc::new(AtomicBool::default()),
 523                        executor,
 524                    )
 525                    .await;
 526
 527                    matches.extend(entry_matches.into_iter().map(|mat| {
 528                        Match::Entry(EntryMatch {
 529                            entry: entries[mat.candidate_id],
 530                            mat: Some(mat),
 531                        })
 532                    }));
 533
 534                    matches.sort_by(|a, b| {
 535                        b.score()
 536                            .partial_cmp(&a.score())
 537                            .unwrap_or(std::cmp::Ordering::Equal)
 538                    });
 539
 540                    matches
 541                })
 542            }
 543        }
 544    }
 545
 546    fn recent_context_picker_entries(
 547        &self,
 548        workspace: &Entity<Workspace>,
 549        cx: &mut App,
 550    ) -> Vec<Match> {
 551        let mut recent = Vec::with_capacity(6);
 552
 553        let mut mentions = self
 554            .message_editor
 555            .read_with(cx, |message_editor, _cx| message_editor.mentions())
 556            .unwrap_or_default();
 557        let workspace = workspace.read(cx);
 558        let project = workspace.project().read(cx);
 559
 560        if let Some(agent_panel) = workspace.panel::<AgentPanel>(cx)
 561            && let Some(thread) = agent_panel.read(cx).active_agent_thread(cx)
 562        {
 563            let thread = thread.read(cx);
 564            mentions.insert(MentionUri::Thread {
 565                id: thread.session_id().clone(),
 566                name: thread.title().into(),
 567            });
 568        }
 569
 570        recent.extend(
 571            workspace
 572                .recent_navigation_history_iter(cx)
 573                .filter(|(_, abs_path)| {
 574                    abs_path.as_ref().is_none_or(|path| {
 575                        !mentions.contains(&MentionUri::File {
 576                            abs_path: path.clone(),
 577                        })
 578                    })
 579                })
 580                .take(4)
 581                .filter_map(|(project_path, _)| {
 582                    project
 583                        .worktree_for_id(project_path.worktree_id, cx)
 584                        .map(|worktree| {
 585                            let path_prefix = worktree.read(cx).root_name().into();
 586                            Match::File(FileMatch {
 587                                mat: fuzzy::PathMatch {
 588                                    score: 1.,
 589                                    positions: Vec::new(),
 590                                    worktree_id: project_path.worktree_id.to_usize(),
 591                                    path: project_path.path,
 592                                    path_prefix,
 593                                    is_dir: false,
 594                                    distance_to_relative_ancestor: 0,
 595                                },
 596                                is_recent: true,
 597                            })
 598                        })
 599                }),
 600        );
 601
 602        if self.prompt_capabilities.get().embedded_context {
 603            const RECENT_COUNT: usize = 2;
 604            let threads = self
 605                .history_store
 606                .read(cx)
 607                .recently_opened_entries(cx)
 608                .into_iter()
 609                .filter(|thread| !mentions.contains(&thread.mention_uri()))
 610                .take(RECENT_COUNT)
 611                .collect::<Vec<_>>();
 612
 613            recent.extend(threads.into_iter().map(Match::RecentThread));
 614        }
 615
 616        recent
 617    }
 618
 619    fn available_context_picker_entries(
 620        &self,
 621        workspace: &Entity<Workspace>,
 622        cx: &mut App,
 623    ) -> Vec<ContextPickerEntry> {
 624        let embedded_context = self.prompt_capabilities.get().embedded_context;
 625        let mut entries = if embedded_context {
 626            vec![
 627                ContextPickerEntry::Mode(ContextPickerMode::File),
 628                ContextPickerEntry::Mode(ContextPickerMode::Symbol),
 629                ContextPickerEntry::Mode(ContextPickerMode::Thread),
 630            ]
 631        } else {
 632            // File is always available, but we don't need a mode entry
 633            vec![]
 634        };
 635
 636        let has_selection = workspace
 637            .read(cx)
 638            .active_item(cx)
 639            .and_then(|item| item.downcast::<Editor>())
 640            .is_some_and(|editor| {
 641                editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))
 642            });
 643        if has_selection {
 644            entries.push(ContextPickerEntry::Action(
 645                ContextPickerAction::AddSelections,
 646            ));
 647        }
 648
 649        if embedded_context {
 650            if self.prompt_store.is_some() {
 651                entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
 652            }
 653
 654            entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
 655        }
 656
 657        entries
 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<Vec<CompletionResponse>>> {
 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            ContextCompletion::try_parse(
 694                line,
 695                offset_to_line,
 696                self.prompt_capabilities.get().embedded_context,
 697            )
 698        });
 699        let Some(state) = state else {
 700            return Task::ready(Ok(Vec::new()));
 701        };
 702
 703        let Some(workspace) = self.workspace.upgrade() else {
 704            return Task::ready(Ok(Vec::new()));
 705        };
 706
 707        let project = workspace.read(cx).project().clone();
 708        let snapshot = buffer.read(cx).snapshot();
 709        let source_range = snapshot.anchor_before(state.source_range().start)
 710            ..snapshot.anchor_after(state.source_range().end);
 711
 712        let editor = self.message_editor.clone();
 713
 714        match state {
 715            ContextCompletion::SlashCommand(SlashCommandCompletion {
 716                command, argument, ..
 717            }) => {
 718                let search_task = self.search_slash_commands(command.unwrap_or_default(), cx);
 719                cx.background_spawn(async move {
 720                    let completions = search_task
 721                        .await
 722                        .into_iter()
 723                        .map(|command| {
 724                            let new_text = if let Some(argument) = argument.as_ref() {
 725                                format!("/{} {}", command.name, argument)
 726                            } else {
 727                                format!("/{} ", command.name)
 728                            };
 729
 730                            let is_missing_argument = argument.is_none() && command.input.is_some();
 731                            Completion {
 732                                replace_range: source_range.clone(),
 733                                new_text,
 734                                label: CodeLabel::plain(command.name.to_string(), None),
 735                                documentation: Some(CompletionDocumentation::SingleLine(
 736                                    command.description.into(),
 737                                )),
 738                                source: project::CompletionSource::Custom,
 739                                icon_path: None,
 740                                insert_text_mode: None,
 741                                confirm: Some(Arc::new({
 742                                    let editor = editor.clone();
 743                                    move |intent, _window, cx| {
 744                                        if !is_missing_argument {
 745                                            cx.defer({
 746                                                let editor = editor.clone();
 747                                                move |cx| {
 748                                                    editor
 749                                                        .update(cx, |_editor, cx| {
 750                                                            match intent {
 751                                                                CompletionIntent::Complete
 752                                                                | CompletionIntent::CompleteWithInsert
 753                                                                | CompletionIntent::CompleteWithReplace => {
 754                                                                    if !is_missing_argument {
 755                                                                        cx.emit(MessageEditorEvent::Send);
 756                                                                    }
 757                                                                }
 758                                                                CompletionIntent::Compose => {}
 759                                                            }
 760                                                        })
 761                                                        .ok();
 762                                                }
 763                                            });
 764                                        }
 765                                        is_missing_argument
 766                                    }
 767                                })),
 768                            }
 769                        })
 770                        .collect();
 771
 772                    Ok(vec![CompletionResponse {
 773                        completions,
 774                        // Since this does its own filtering (see `filter_completions()` returns false),
 775                        // there is no benefit to computing whether this set of completions is incomplete.
 776                        is_incomplete: true,
 777                    }])
 778                })
 779            }
 780            ContextCompletion::Mention(MentionCompletion { mode, argument, .. }) => {
 781                let query = argument.unwrap_or_default();
 782                let search_task =
 783                    self.search_mentions(mode, query, Arc::<AtomicBool>::default(), cx);
 784
 785                cx.spawn(async move |_, cx| {
 786                    let matches = search_task.await;
 787
 788                    let completions = cx.update(|cx| {
 789                        matches
 790                            .into_iter()
 791                            .filter_map(|mat| match mat {
 792                                Match::File(FileMatch { mat, is_recent }) => {
 793                                    let project_path = ProjectPath {
 794                                        worktree_id: WorktreeId::from_usize(mat.worktree_id),
 795                                        path: mat.path.clone(),
 796                                    };
 797
 798                                    Self::completion_for_path(
 799                                        project_path,
 800                                        &mat.path_prefix,
 801                                        is_recent,
 802                                        mat.is_dir,
 803                                        source_range.clone(),
 804                                        editor.clone(),
 805                                        project.clone(),
 806                                        cx,
 807                                    )
 808                                }
 809
 810                                Match::Symbol(SymbolMatch { symbol, .. }) => {
 811                                    Self::completion_for_symbol(
 812                                        symbol,
 813                                        source_range.clone(),
 814                                        editor.clone(),
 815                                        workspace.clone(),
 816                                        cx,
 817                                    )
 818                                }
 819
 820                                Match::Thread(thread) => Some(Self::completion_for_thread(
 821                                    thread,
 822                                    source_range.clone(),
 823                                    false,
 824                                    editor.clone(),
 825                                    cx,
 826                                )),
 827
 828                                Match::RecentThread(thread) => Some(Self::completion_for_thread(
 829                                    thread,
 830                                    source_range.clone(),
 831                                    true,
 832                                    editor.clone(),
 833                                    cx,
 834                                )),
 835
 836                                Match::Rules(user_rules) => Some(Self::completion_for_rules(
 837                                    user_rules,
 838                                    source_range.clone(),
 839                                    editor.clone(),
 840                                    cx,
 841                                )),
 842
 843                                Match::Fetch(url) => Self::completion_for_fetch(
 844                                    source_range.clone(),
 845                                    url,
 846                                    editor.clone(),
 847                                    cx,
 848                                ),
 849
 850                                Match::Entry(EntryMatch { entry, .. }) => {
 851                                    Self::completion_for_entry(
 852                                        entry,
 853                                        source_range.clone(),
 854                                        editor.clone(),
 855                                        &workspace,
 856                                        cx,
 857                                    )
 858                                }
 859                            })
 860                            .collect()
 861                    })?;
 862
 863                    Ok(vec![CompletionResponse {
 864                        completions,
 865                        // Since this does its own filtering (see `filter_completions()` returns false),
 866                        // there is no benefit to computing whether this set of completions is incomplete.
 867                        is_incomplete: true,
 868                    }])
 869                })
 870            }
 871        }
 872    }
 873
 874    fn is_completion_trigger(
 875        &self,
 876        buffer: &Entity<language::Buffer>,
 877        position: language::Anchor,
 878        _text: &str,
 879        _trigger_in_words: bool,
 880        _menu_is_open: bool,
 881        cx: &mut Context<Editor>,
 882    ) -> bool {
 883        let buffer = buffer.read(cx);
 884        let position = position.to_point(buffer);
 885        let line_start = Point::new(position.row, 0);
 886        let offset_to_line = buffer.point_to_offset(line_start);
 887        let mut lines = buffer.text_for_range(line_start..position).lines();
 888        if let Some(line) = lines.next() {
 889            ContextCompletion::try_parse(
 890                line,
 891                offset_to_line,
 892                self.prompt_capabilities.get().embedded_context,
 893            )
 894            .map(|completion| {
 895                completion.source_range().start <= offset_to_line + position.column as usize
 896                    && completion.source_range().end >= offset_to_line + position.column as usize
 897            })
 898            .unwrap_or(false)
 899        } else {
 900            false
 901        }
 902    }
 903
 904    fn sort_completions(&self) -> bool {
 905        false
 906    }
 907
 908    fn filter_completions(&self) -> bool {
 909        false
 910    }
 911}
 912
 913pub(crate) fn search_threads(
 914    query: String,
 915    cancellation_flag: Arc<AtomicBool>,
 916    history_store: &Entity<HistoryStore>,
 917    cx: &mut App,
 918) -> Task<Vec<HistoryEntry>> {
 919    let threads = history_store.read(cx).entries().collect();
 920    if query.is_empty() {
 921        return Task::ready(threads);
 922    }
 923
 924    let executor = cx.background_executor().clone();
 925    cx.background_spawn(async move {
 926        let candidates = threads
 927            .iter()
 928            .enumerate()
 929            .map(|(id, thread)| StringMatchCandidate::new(id, thread.title()))
 930            .collect::<Vec<_>>();
 931        let matches = fuzzy::match_strings(
 932            &candidates,
 933            &query,
 934            false,
 935            true,
 936            100,
 937            &cancellation_flag,
 938            executor,
 939        )
 940        .await;
 941
 942        matches
 943            .into_iter()
 944            .map(|mat| threads[mat.candidate_id].clone())
 945            .collect()
 946    })
 947}
 948
 949fn confirm_completion_callback(
 950    crease_text: SharedString,
 951    start: Anchor,
 952    content_len: usize,
 953    message_editor: WeakEntity<MessageEditor>,
 954    mention_uri: MentionUri,
 955) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
 956    Arc::new(move |_, window, cx| {
 957        let message_editor = message_editor.clone();
 958        let crease_text = crease_text.clone();
 959        let mention_uri = mention_uri.clone();
 960        window.defer(cx, move |window, cx| {
 961            message_editor
 962                .clone()
 963                .update(cx, |message_editor, cx| {
 964                    message_editor
 965                        .confirm_mention_completion(
 966                            crease_text,
 967                            start,
 968                            content_len,
 969                            mention_uri,
 970                            window,
 971                            cx,
 972                        )
 973                        .detach();
 974                })
 975                .ok();
 976        });
 977        false
 978    })
 979}
 980
 981enum ContextCompletion {
 982    SlashCommand(SlashCommandCompletion),
 983    Mention(MentionCompletion),
 984}
 985
 986impl ContextCompletion {
 987    fn source_range(&self) -> Range<usize> {
 988        match self {
 989            Self::SlashCommand(completion) => completion.source_range.clone(),
 990            Self::Mention(completion) => completion.source_range.clone(),
 991        }
 992    }
 993
 994    fn try_parse(line: &str, offset_to_line: usize, allow_non_file_mentions: bool) -> Option<Self> {
 995        if let Some(command) = SlashCommandCompletion::try_parse(line, offset_to_line) {
 996            Some(Self::SlashCommand(command))
 997        } else if let Some(mention) =
 998            MentionCompletion::try_parse(allow_non_file_mentions, line, offset_to_line)
 999        {
1000            Some(Self::Mention(mention))
1001        } else {
1002            None
1003        }
1004    }
1005}
1006
1007#[derive(Debug, Default, PartialEq)]
1008pub struct SlashCommandCompletion {
1009    pub source_range: Range<usize>,
1010    pub command: Option<String>,
1011    pub argument: Option<String>,
1012}
1013
1014impl SlashCommandCompletion {
1015    pub fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
1016        // If we decide to support commands that are not at the beginning of the prompt, we can remove this check
1017        if !line.starts_with('/') || offset_to_line != 0 {
1018            return None;
1019        }
1020
1021        let last_command_start = line.rfind('/')?;
1022        if last_command_start >= line.len() {
1023            return Some(Self::default());
1024        }
1025        if last_command_start > 0
1026            && line
1027                .chars()
1028                .nth(last_command_start - 1)
1029                .is_some_and(|c| !c.is_whitespace())
1030        {
1031            return None;
1032        }
1033
1034        let rest_of_line = &line[last_command_start + 1..];
1035
1036        let mut command = None;
1037        let mut argument = None;
1038        let mut end = last_command_start + 1;
1039
1040        if let Some(command_text) = rest_of_line.split_whitespace().next() {
1041            command = Some(command_text.to_string());
1042            end += command_text.len();
1043
1044            // Find the start of arguments after the command
1045            if let Some(args_start) =
1046                rest_of_line[command_text.len()..].find(|c: char| !c.is_whitespace())
1047            {
1048                let args = &rest_of_line[command_text.len() + args_start..].trim_end();
1049                if !args.is_empty() {
1050                    argument = Some(args.to_string());
1051                    end += args.len() + 1;
1052                }
1053            }
1054        }
1055
1056        Some(Self {
1057            source_range: last_command_start + offset_to_line..end + offset_to_line,
1058            command,
1059            argument,
1060        })
1061    }
1062}
1063
1064#[derive(Debug, Default, PartialEq)]
1065struct MentionCompletion {
1066    source_range: Range<usize>,
1067    mode: Option<ContextPickerMode>,
1068    argument: Option<String>,
1069}
1070
1071impl MentionCompletion {
1072    fn try_parse(allow_non_file_mentions: bool, line: &str, offset_to_line: usize) -> Option<Self> {
1073        let last_mention_start = line.rfind('@')?;
1074        if last_mention_start >= line.len() {
1075            return Some(Self::default());
1076        }
1077        if last_mention_start > 0
1078            && line
1079                .chars()
1080                .nth(last_mention_start - 1)
1081                .is_some_and(|c| !c.is_whitespace())
1082        {
1083            return None;
1084        }
1085
1086        let rest_of_line = &line[last_mention_start + 1..];
1087
1088        let mut mode = None;
1089        let mut argument = None;
1090
1091        let mut parts = rest_of_line.split_whitespace();
1092        let mut end = last_mention_start + 1;
1093        if let Some(mode_text) = parts.next() {
1094            end += mode_text.len();
1095
1096            if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok()
1097                && (allow_non_file_mentions || matches!(parsed_mode, ContextPickerMode::File))
1098            {
1099                mode = Some(parsed_mode);
1100            } else {
1101                argument = Some(mode_text.to_string());
1102            }
1103            match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
1104                Some(whitespace_count) => {
1105                    if let Some(argument_text) = parts.next() {
1106                        argument = Some(argument_text.to_string());
1107                        end += whitespace_count + argument_text.len();
1108                    }
1109                }
1110                None => {
1111                    // Rest of line is entirely whitespace
1112                    end += rest_of_line.len() - mode_text.len();
1113                }
1114            }
1115        }
1116
1117        Some(Self {
1118            source_range: last_mention_start + offset_to_line..end + offset_to_line,
1119            mode,
1120            argument,
1121        })
1122    }
1123}
1124
1125#[cfg(test)]
1126mod tests {
1127    use super::*;
1128
1129    #[test]
1130    fn test_slash_command_completion_parse() {
1131        assert_eq!(
1132            SlashCommandCompletion::try_parse("/", 0),
1133            Some(SlashCommandCompletion {
1134                source_range: 0..1,
1135                command: None,
1136                argument: None,
1137            })
1138        );
1139
1140        assert_eq!(
1141            SlashCommandCompletion::try_parse("/help", 0),
1142            Some(SlashCommandCompletion {
1143                source_range: 0..5,
1144                command: Some("help".to_string()),
1145                argument: None,
1146            })
1147        );
1148
1149        assert_eq!(
1150            SlashCommandCompletion::try_parse("/help ", 0),
1151            Some(SlashCommandCompletion {
1152                source_range: 0..5,
1153                command: Some("help".to_string()),
1154                argument: None,
1155            })
1156        );
1157
1158        assert_eq!(
1159            SlashCommandCompletion::try_parse("/help arg1", 0),
1160            Some(SlashCommandCompletion {
1161                source_range: 0..10,
1162                command: Some("help".to_string()),
1163                argument: Some("arg1".to_string()),
1164            })
1165        );
1166
1167        assert_eq!(
1168            SlashCommandCompletion::try_parse("/help arg1 arg2", 0),
1169            Some(SlashCommandCompletion {
1170                source_range: 0..15,
1171                command: Some("help".to_string()),
1172                argument: Some("arg1 arg2".to_string()),
1173            })
1174        );
1175
1176        assert_eq!(SlashCommandCompletion::try_parse("Lorem Ipsum", 0), None);
1177
1178        assert_eq!(SlashCommandCompletion::try_parse("Lorem /", 0), None);
1179
1180        assert_eq!(SlashCommandCompletion::try_parse("Lorem /help", 0), None);
1181
1182        assert_eq!(SlashCommandCompletion::try_parse("Lorem/", 0), None);
1183    }
1184
1185    #[test]
1186    fn test_mention_completion_parse() {
1187        assert_eq!(MentionCompletion::try_parse(true, "Lorem Ipsum", 0), None);
1188
1189        assert_eq!(
1190            MentionCompletion::try_parse(true, "Lorem @", 0),
1191            Some(MentionCompletion {
1192                source_range: 6..7,
1193                mode: None,
1194                argument: None,
1195            })
1196        );
1197
1198        assert_eq!(
1199            MentionCompletion::try_parse(true, "Lorem @file", 0),
1200            Some(MentionCompletion {
1201                source_range: 6..11,
1202                mode: Some(ContextPickerMode::File),
1203                argument: None,
1204            })
1205        );
1206
1207        assert_eq!(
1208            MentionCompletion::try_parse(true, "Lorem @file ", 0),
1209            Some(MentionCompletion {
1210                source_range: 6..12,
1211                mode: Some(ContextPickerMode::File),
1212                argument: None,
1213            })
1214        );
1215
1216        assert_eq!(
1217            MentionCompletion::try_parse(true, "Lorem @file main.rs", 0),
1218            Some(MentionCompletion {
1219                source_range: 6..19,
1220                mode: Some(ContextPickerMode::File),
1221                argument: Some("main.rs".to_string()),
1222            })
1223        );
1224
1225        assert_eq!(
1226            MentionCompletion::try_parse(true, "Lorem @file main.rs ", 0),
1227            Some(MentionCompletion {
1228                source_range: 6..19,
1229                mode: Some(ContextPickerMode::File),
1230                argument: Some("main.rs".to_string()),
1231            })
1232        );
1233
1234        assert_eq!(
1235            MentionCompletion::try_parse(true, "Lorem @file main.rs Ipsum", 0),
1236            Some(MentionCompletion {
1237                source_range: 6..19,
1238                mode: Some(ContextPickerMode::File),
1239                argument: Some("main.rs".to_string()),
1240            })
1241        );
1242
1243        assert_eq!(
1244            MentionCompletion::try_parse(true, "Lorem @main", 0),
1245            Some(MentionCompletion {
1246                source_range: 6..11,
1247                mode: None,
1248                argument: Some("main".to_string()),
1249            })
1250        );
1251
1252        assert_eq!(MentionCompletion::try_parse(true, "test@", 0), None);
1253
1254        // Allowed non-file mentions
1255
1256        assert_eq!(
1257            MentionCompletion::try_parse(true, "Lorem @symbol main", 0),
1258            Some(MentionCompletion {
1259                source_range: 6..18,
1260                mode: Some(ContextPickerMode::Symbol),
1261                argument: Some("main".to_string()),
1262            })
1263        );
1264
1265        // Disallowed non-file mentions
1266
1267        assert_eq!(
1268            MentionCompletion::try_parse(false, "Lorem @symbol main", 0),
1269            Some(MentionCompletion {
1270                source_range: 6..18,
1271                mode: None,
1272                argument: Some("main".to_string()),
1273            })
1274        );
1275    }
1276}