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