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