completion_provider.rs

   1use std::cmp::Reverse;
   2use std::ops::Range;
   3use std::path::PathBuf;
   4use std::sync::Arc;
   5use std::sync::atomic::AtomicBool;
   6
   7use crate::DEFAULT_THREAD_TITLE;
   8use crate::ThreadHistory;
   9use acp_thread::MentionUri;
  10use agent_client_protocol as acp;
  11use anyhow::Result;
  12use editor::{
  13    CompletionProvider, Editor, ExcerptId, code_context_menus::COMPLETION_MENU_MAX_WIDTH,
  14};
  15use futures::FutureExt as _;
  16use fuzzy::{PathMatch, StringMatch, StringMatchCandidate};
  17use gpui::{App, BackgroundExecutor, Entity, SharedString, Task, WeakEntity};
  18use language::{Buffer, CodeLabel, CodeLabelBuilder, HighlightId};
  19use lsp::CompletionContext;
  20use multi_buffer::ToOffset as _;
  21use ordered_float::OrderedFloat;
  22use project::lsp_store::{CompletionDocumentation, SymbolLocation};
  23use project::{
  24    Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, DiagnosticSummary,
  25    PathMatchCandidateSet, Project, ProjectPath, Symbol, WorktreeId,
  26};
  27use prompt_store::{PromptStore, UserPromptId};
  28use rope::Point;
  29use settings::{Settings, TerminalDockPosition};
  30use terminal::terminal_settings::TerminalSettings;
  31use terminal_view::terminal_panel::TerminalPanel;
  32use text::{Anchor, ToOffset as _, ToPoint as _};
  33use ui::IconName;
  34use ui::prelude::*;
  35use util::ResultExt as _;
  36use util::paths::PathStyle;
  37use util::rel_path::RelPath;
  38use util::truncate_and_remove_front;
  39use workspace::Workspace;
  40use workspace::dock::DockPosition;
  41
  42use crate::AgentPanel;
  43use crate::mention_set::MentionSet;
  44
  45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
  46pub(crate) enum PromptContextEntry {
  47    Mode(PromptContextType),
  48    Action(PromptContextAction),
  49}
  50
  51impl PromptContextEntry {
  52    pub fn keyword(&self) -> &'static str {
  53        match self {
  54            Self::Mode(mode) => mode.keyword(),
  55            Self::Action(action) => action.keyword(),
  56        }
  57    }
  58}
  59
  60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
  61pub(crate) enum PromptContextType {
  62    File,
  63    Symbol,
  64    Fetch,
  65    Thread,
  66    Rules,
  67    Diagnostics,
  68    BranchDiff,
  69}
  70
  71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
  72pub(crate) enum PromptContextAction {
  73    AddSelections,
  74}
  75
  76impl PromptContextAction {
  77    pub fn keyword(&self) -> &'static str {
  78        match self {
  79            Self::AddSelections => "selection",
  80        }
  81    }
  82
  83    pub fn label(&self) -> &'static str {
  84        match self {
  85            Self::AddSelections => "Selection",
  86        }
  87    }
  88
  89    pub fn icon(&self) -> IconName {
  90        match self {
  91            Self::AddSelections => IconName::Reader,
  92        }
  93    }
  94}
  95
  96impl TryFrom<&str> for PromptContextType {
  97    type Error = String;
  98
  99    fn try_from(value: &str) -> Result<Self, Self::Error> {
 100        match value {
 101            "file" => Ok(Self::File),
 102            "symbol" => Ok(Self::Symbol),
 103            "fetch" => Ok(Self::Fetch),
 104            "thread" => Ok(Self::Thread),
 105            "rule" => Ok(Self::Rules),
 106            "diagnostics" => Ok(Self::Diagnostics),
 107            "diff" => Ok(Self::BranchDiff),
 108            _ => Err(format!("Invalid context picker mode: {}", value)),
 109        }
 110    }
 111}
 112
 113impl PromptContextType {
 114    pub fn keyword(&self) -> &'static str {
 115        match self {
 116            Self::File => "file",
 117            Self::Symbol => "symbol",
 118            Self::Fetch => "fetch",
 119            Self::Thread => "thread",
 120            Self::Rules => "rule",
 121            Self::Diagnostics => "diagnostics",
 122            Self::BranchDiff => "branch diff",
 123        }
 124    }
 125
 126    pub fn label(&self) -> &'static str {
 127        match self {
 128            Self::File => "Files & Directories",
 129            Self::Symbol => "Symbols",
 130            Self::Fetch => "Fetch",
 131            Self::Thread => "Threads",
 132            Self::Rules => "Rules",
 133            Self::Diagnostics => "Diagnostics",
 134            Self::BranchDiff => "Branch Diff",
 135        }
 136    }
 137
 138    pub fn icon(&self) -> IconName {
 139        match self {
 140            Self::File => IconName::File,
 141            Self::Symbol => IconName::Code,
 142            Self::Fetch => IconName::ToolWeb,
 143            Self::Thread => IconName::Thread,
 144            Self::Rules => IconName::Reader,
 145            Self::Diagnostics => IconName::Warning,
 146            Self::BranchDiff => IconName::GitBranch,
 147        }
 148    }
 149}
 150
 151pub(crate) enum Match {
 152    File(FileMatch),
 153    Symbol(SymbolMatch),
 154    Thread(SessionMatch),
 155    RecentThread(SessionMatch),
 156    Fetch(SharedString),
 157    Rules(RulesContextEntry),
 158    Entry(EntryMatch),
 159    BranchDiff(BranchDiffMatch),
 160}
 161
 162#[derive(Debug, Clone)]
 163pub struct BranchDiffMatch {
 164    pub base_ref: SharedString,
 165}
 166
 167impl Match {
 168    pub fn score(&self) -> f64 {
 169        match self {
 170            Match::File(file) => file.mat.score,
 171            Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
 172            Match::Thread(_) => 1.,
 173            Match::RecentThread(_) => 1.,
 174            Match::Symbol(_) => 1.,
 175            Match::Rules(_) => 1.,
 176            Match::Fetch(_) => 1.,
 177            Match::BranchDiff(_) => 1.,
 178        }
 179    }
 180}
 181
 182#[derive(Debug, Clone)]
 183pub struct SessionMatch {
 184    session_id: acp::SessionId,
 185    title: SharedString,
 186}
 187
 188pub struct EntryMatch {
 189    mat: Option<StringMatch>,
 190    entry: PromptContextEntry,
 191}
 192
 193fn session_title(title: Option<SharedString>) -> SharedString {
 194    title
 195        .filter(|title| !title.is_empty())
 196        .unwrap_or_else(|| SharedString::new_static(DEFAULT_THREAD_TITLE))
 197}
 198
 199#[derive(Debug, Clone)]
 200pub struct RulesContextEntry {
 201    pub prompt_id: UserPromptId,
 202    pub title: SharedString,
 203}
 204
 205#[derive(Debug, Clone)]
 206pub struct AvailableCommand {
 207    pub name: Arc<str>,
 208    pub description: Arc<str>,
 209    pub requires_argument: bool,
 210}
 211
 212pub trait PromptCompletionProviderDelegate: Send + Sync + 'static {
 213    fn supports_context(&self, mode: PromptContextType, cx: &App) -> bool {
 214        self.supported_modes(cx).contains(&mode)
 215    }
 216    fn supported_modes(&self, cx: &App) -> Vec<PromptContextType>;
 217    fn supports_images(&self, cx: &App) -> bool;
 218
 219    fn available_commands(&self, cx: &App) -> Vec<AvailableCommand>;
 220    fn confirm_command(&self, cx: &mut App);
 221}
 222
 223pub struct PromptCompletionProvider<T: PromptCompletionProviderDelegate> {
 224    source: Arc<T>,
 225    editor: WeakEntity<Editor>,
 226    mention_set: Entity<MentionSet>,
 227    history: Option<WeakEntity<ThreadHistory>>,
 228    prompt_store: Option<Entity<PromptStore>>,
 229    workspace: WeakEntity<Workspace>,
 230}
 231
 232impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
 233    pub fn new(
 234        source: T,
 235        editor: WeakEntity<Editor>,
 236        mention_set: Entity<MentionSet>,
 237        history: Option<WeakEntity<ThreadHistory>>,
 238        prompt_store: Option<Entity<PromptStore>>,
 239        workspace: WeakEntity<Workspace>,
 240    ) -> Self {
 241        Self {
 242            source: Arc::new(source),
 243            editor,
 244            mention_set,
 245            workspace,
 246            history,
 247            prompt_store,
 248        }
 249    }
 250
 251    fn completion_for_entry(
 252        entry: PromptContextEntry,
 253        source_range: Range<Anchor>,
 254        editor: WeakEntity<Editor>,
 255        mention_set: WeakEntity<MentionSet>,
 256        workspace: &Entity<Workspace>,
 257        cx: &mut App,
 258    ) -> Option<Completion> {
 259        match entry {
 260            PromptContextEntry::Mode(mode) => Some(Completion {
 261                replace_range: source_range,
 262                new_text: format!("@{} ", mode.keyword()),
 263                label: CodeLabel::plain(mode.label().to_string(), None),
 264                icon_path: Some(mode.icon().path().into()),
 265                documentation: None,
 266                source: project::CompletionSource::Custom,
 267                match_start: None,
 268                snippet_deduplication_key: None,
 269                insert_text_mode: None,
 270                // This ensures that when a user accepts this completion, the
 271                // completion menu will still be shown after "@category " is
 272                // inserted
 273                confirm: Some(Arc::new(|_, _, _| true)),
 274            }),
 275            PromptContextEntry::Action(action) => Self::completion_for_action(
 276                action,
 277                source_range,
 278                editor,
 279                mention_set,
 280                workspace,
 281                cx,
 282            ),
 283        }
 284    }
 285
 286    fn completion_for_thread(
 287        session_id: acp::SessionId,
 288        title: Option<SharedString>,
 289        source_range: Range<Anchor>,
 290        recent: bool,
 291        source: Arc<T>,
 292        editor: WeakEntity<Editor>,
 293        mention_set: WeakEntity<MentionSet>,
 294        workspace: Entity<Workspace>,
 295        cx: &mut App,
 296    ) -> Completion {
 297        let title = session_title(title);
 298        let uri = MentionUri::Thread {
 299            id: session_id,
 300            name: title.to_string(),
 301        };
 302
 303        let icon_for_completion = if recent {
 304            IconName::HistoryRerun.path().into()
 305        } else {
 306            uri.icon_path(cx)
 307        };
 308
 309        let new_text = format!("{} ", uri.as_link());
 310
 311        let new_text_len = new_text.len();
 312        Completion {
 313            replace_range: source_range.clone(),
 314            new_text,
 315            label: CodeLabel::plain(title.to_string(), None),
 316            documentation: None,
 317            insert_text_mode: None,
 318            source: project::CompletionSource::Custom,
 319            match_start: None,
 320            snippet_deduplication_key: None,
 321            icon_path: Some(icon_for_completion),
 322            confirm: Some(confirm_completion_callback(
 323                title,
 324                source_range.start,
 325                new_text_len - 1,
 326                uri,
 327                source,
 328                editor,
 329                mention_set,
 330                workspace,
 331            )),
 332        }
 333    }
 334
 335    fn completion_for_rules(
 336        rule: RulesContextEntry,
 337        source_range: Range<Anchor>,
 338        source: Arc<T>,
 339        editor: WeakEntity<Editor>,
 340        mention_set: WeakEntity<MentionSet>,
 341        workspace: Entity<Workspace>,
 342        cx: &mut App,
 343    ) -> Completion {
 344        let uri = MentionUri::Rule {
 345            id: rule.prompt_id.into(),
 346            name: rule.title.to_string(),
 347        };
 348        let new_text = format!("{} ", uri.as_link());
 349        let new_text_len = new_text.len();
 350        let icon_path = uri.icon_path(cx);
 351        Completion {
 352            replace_range: source_range.clone(),
 353            new_text,
 354            label: CodeLabel::plain(rule.title.to_string(), None),
 355            documentation: None,
 356            insert_text_mode: None,
 357            source: project::CompletionSource::Custom,
 358            match_start: None,
 359            snippet_deduplication_key: None,
 360            icon_path: Some(icon_path),
 361            confirm: Some(confirm_completion_callback(
 362                rule.title,
 363                source_range.start,
 364                new_text_len - 1,
 365                uri,
 366                source,
 367                editor,
 368                mention_set,
 369                workspace,
 370            )),
 371        }
 372    }
 373
 374    pub(crate) fn completion_for_path(
 375        project_path: ProjectPath,
 376        path_prefix: &RelPath,
 377        is_recent: bool,
 378        is_directory: bool,
 379        source_range: Range<Anchor>,
 380        source: Arc<T>,
 381        editor: WeakEntity<Editor>,
 382        mention_set: WeakEntity<MentionSet>,
 383        workspace: Entity<Workspace>,
 384        project: Entity<Project>,
 385        label_max_chars: usize,
 386        cx: &mut App,
 387    ) -> Option<Completion> {
 388        let path_style = project.read(cx).path_style(cx);
 389        let (file_name, directory) =
 390            extract_file_name_and_directory(&project_path.path, path_prefix, path_style);
 391
 392        let label = build_code_label_for_path(
 393            &file_name,
 394            directory.as_ref().map(|s| s.as_ref()),
 395            None,
 396            label_max_chars,
 397            cx,
 398        );
 399
 400        let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
 401
 402        let uri = if is_directory {
 403            MentionUri::Directory { abs_path }
 404        } else {
 405            MentionUri::File { abs_path }
 406        };
 407
 408        let crease_icon_path = uri.icon_path(cx);
 409        let completion_icon_path = if is_recent {
 410            IconName::HistoryRerun.path().into()
 411        } else {
 412            crease_icon_path
 413        };
 414
 415        let new_text = format!("{} ", uri.as_link());
 416        let new_text_len = new_text.len();
 417        Some(Completion {
 418            replace_range: source_range.clone(),
 419            new_text,
 420            label,
 421            documentation: None,
 422            source: project::CompletionSource::Custom,
 423            icon_path: Some(completion_icon_path),
 424            match_start: None,
 425            snippet_deduplication_key: None,
 426            insert_text_mode: None,
 427            confirm: Some(confirm_completion_callback(
 428                file_name,
 429                source_range.start,
 430                new_text_len - 1,
 431                uri,
 432                source,
 433                editor,
 434                mention_set,
 435                workspace,
 436            )),
 437        })
 438    }
 439
 440    fn completion_for_symbol(
 441        symbol: Symbol,
 442        source_range: Range<Anchor>,
 443        source: Arc<T>,
 444        editor: WeakEntity<Editor>,
 445        mention_set: WeakEntity<MentionSet>,
 446        workspace: Entity<Workspace>,
 447        label_max_chars: usize,
 448        cx: &mut App,
 449    ) -> Option<Completion> {
 450        let project = workspace.read(cx).project().clone();
 451
 452        let (abs_path, file_name) = match &symbol.path {
 453            SymbolLocation::InProject(project_path) => (
 454                project.read(cx).absolute_path(&project_path, cx)?,
 455                project_path.path.file_name()?.to_string().into(),
 456            ),
 457            SymbolLocation::OutsideProject {
 458                abs_path,
 459                signature: _,
 460            } => (
 461                PathBuf::from(abs_path.as_ref()),
 462                abs_path.file_name().map(|f| f.to_string_lossy())?,
 463            ),
 464        };
 465
 466        let label = build_code_label_for_path(
 467            &symbol.name,
 468            Some(&file_name),
 469            Some(symbol.range.start.0.row + 1),
 470            label_max_chars,
 471            cx,
 472        );
 473
 474        let uri = MentionUri::Symbol {
 475            abs_path,
 476            name: symbol.name.clone(),
 477            line_range: symbol.range.start.0.row..=symbol.range.end.0.row,
 478        };
 479        let new_text = format!("{} ", uri.as_link());
 480        let new_text_len = new_text.len();
 481        let icon_path = uri.icon_path(cx);
 482        Some(Completion {
 483            replace_range: source_range.clone(),
 484            new_text,
 485            label,
 486            documentation: None,
 487            source: project::CompletionSource::Custom,
 488            icon_path: Some(icon_path),
 489            match_start: None,
 490            snippet_deduplication_key: None,
 491            insert_text_mode: None,
 492            confirm: Some(confirm_completion_callback(
 493                symbol.name.into(),
 494                source_range.start,
 495                new_text_len - 1,
 496                uri,
 497                source,
 498                editor,
 499                mention_set,
 500                workspace,
 501            )),
 502        })
 503    }
 504
 505    fn completion_for_fetch(
 506        source_range: Range<Anchor>,
 507        url_to_fetch: SharedString,
 508        source: Arc<T>,
 509        editor: WeakEntity<Editor>,
 510        mention_set: WeakEntity<MentionSet>,
 511        workspace: Entity<Workspace>,
 512        cx: &mut App,
 513    ) -> Option<Completion> {
 514        let new_text = format!("@fetch {} ", url_to_fetch);
 515        let url_to_fetch = url::Url::parse(url_to_fetch.as_ref())
 516            .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}")))
 517            .ok()?;
 518        let mention_uri = MentionUri::Fetch {
 519            url: url_to_fetch.clone(),
 520        };
 521        let icon_path = mention_uri.icon_path(cx);
 522        Some(Completion {
 523            replace_range: source_range.clone(),
 524            new_text: new_text.clone(),
 525            label: CodeLabel::plain(url_to_fetch.to_string(), None),
 526            documentation: None,
 527            source: project::CompletionSource::Custom,
 528            icon_path: Some(icon_path),
 529            match_start: None,
 530            snippet_deduplication_key: None,
 531            insert_text_mode: None,
 532            confirm: Some(confirm_completion_callback(
 533                url_to_fetch.to_string().into(),
 534                source_range.start,
 535                new_text.len() - 1,
 536                mention_uri,
 537                source,
 538                editor,
 539                mention_set,
 540                workspace,
 541            )),
 542        })
 543    }
 544
 545    pub(crate) fn completion_for_action(
 546        action: PromptContextAction,
 547        source_range: Range<Anchor>,
 548        editor: WeakEntity<Editor>,
 549        mention_set: WeakEntity<MentionSet>,
 550        workspace: &Entity<Workspace>,
 551        cx: &mut App,
 552    ) -> Option<Completion> {
 553        let (new_text, on_action) = match action {
 554            PromptContextAction::AddSelections => {
 555                // Collect non-empty editor selections
 556                let editor_selections: Vec<_> = selection_ranges(workspace, cx)
 557                    .into_iter()
 558                    .filter(|(buffer, range)| {
 559                        let snapshot = buffer.read(cx).snapshot();
 560                        range.start.to_offset(&snapshot) != range.end.to_offset(&snapshot)
 561                    })
 562                    .collect();
 563
 564                // Collect terminal selections from all terminal views if the terminal panel is visible
 565                let terminal_selections: Vec<String> =
 566                    terminal_selections_if_panel_open(workspace, cx);
 567
 568                const EDITOR_PLACEHOLDER: &str = "selection ";
 569                const TERMINAL_PLACEHOLDER: &str = "terminal ";
 570
 571                let selections = editor_selections
 572                    .into_iter()
 573                    .enumerate()
 574                    .map(|(ix, (buffer, range))| {
 575                        (
 576                            buffer,
 577                            range,
 578                            (EDITOR_PLACEHOLDER.len() * ix)
 579                                ..(EDITOR_PLACEHOLDER.len() * (ix + 1) - 1),
 580                        )
 581                    })
 582                    .collect::<Vec<_>>();
 583
 584                let mut new_text: String = EDITOR_PLACEHOLDER.repeat(selections.len());
 585
 586                // Add terminal placeholders for each terminal selection
 587                let terminal_ranges: Vec<(String, std::ops::Range<usize>)> = terminal_selections
 588                    .into_iter()
 589                    .map(|text| {
 590                        let start = new_text.len();
 591                        new_text.push_str(TERMINAL_PLACEHOLDER);
 592                        (text, start..(new_text.len() - 1))
 593                    })
 594                    .collect();
 595
 596                let callback = Arc::new({
 597                    let source_range = source_range.clone();
 598                    move |_: CompletionIntent, window: &mut Window, cx: &mut App| {
 599                        let editor = editor.clone();
 600                        let selections = selections.clone();
 601                        let mention_set = mention_set.clone();
 602                        let source_range = source_range.clone();
 603                        let terminal_ranges = terminal_ranges.clone();
 604                        window.defer(cx, move |window, cx| {
 605                            if let Some(editor) = editor.upgrade() {
 606                                // Insert editor selections
 607                                if !selections.is_empty() {
 608                                    mention_set
 609                                        .update(cx, |store, cx| {
 610                                            store.confirm_mention_for_selection(
 611                                                source_range.clone(),
 612                                                selections,
 613                                                editor.clone(),
 614                                                window,
 615                                                cx,
 616                                            )
 617                                        })
 618                                        .ok();
 619                                }
 620
 621                                // Insert terminal selections
 622                                for (terminal_text, terminal_range) in terminal_ranges {
 623                                    let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
 624                                    let Some(start) =
 625                                        snapshot.as_singleton_anchor(source_range.start)
 626                                    else {
 627                                        return;
 628                                    };
 629                                    let offset = start.to_offset(&snapshot);
 630
 631                                    let line_count = terminal_text.lines().count() as u32;
 632                                    let mention_uri = MentionUri::TerminalSelection { line_count };
 633                                    let range = snapshot.anchor_after(offset + terminal_range.start)
 634                                        ..snapshot.anchor_after(offset + terminal_range.end);
 635
 636                                    let crease = crate::mention_set::crease_for_mention(
 637                                        mention_uri.name().into(),
 638                                        mention_uri.icon_path(cx),
 639                                        None,
 640                                        range,
 641                                        editor.downgrade(),
 642                                    );
 643
 644                                    let crease_id = editor.update(cx, |editor, cx| {
 645                                        let crease_ids =
 646                                            editor.insert_creases(vec![crease.clone()], cx);
 647                                        editor.fold_creases(vec![crease], false, window, cx);
 648                                        crease_ids.first().copied().unwrap()
 649                                    });
 650
 651                                    mention_set
 652                                        .update(cx, |mention_set, _| {
 653                                            mention_set.insert_mention(
 654                                                crease_id,
 655                                                mention_uri.clone(),
 656                                                gpui::Task::ready(Ok(
 657                                                    crate::mention_set::Mention::Text {
 658                                                        content: terminal_text,
 659                                                        tracked_buffers: vec![],
 660                                                    },
 661                                                ))
 662                                                .shared(),
 663                                            );
 664                                        })
 665                                        .ok();
 666                                }
 667                            }
 668                        });
 669                        false
 670                    }
 671                });
 672
 673                (
 674                    new_text,
 675                    callback
 676                        as Arc<
 677                            dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync,
 678                        >,
 679                )
 680            }
 681        };
 682
 683        Some(Completion {
 684            replace_range: source_range,
 685            new_text,
 686            label: CodeLabel::plain(action.label().to_string(), None),
 687            icon_path: Some(action.icon().path().into()),
 688            documentation: None,
 689            source: project::CompletionSource::Custom,
 690            match_start: None,
 691            snippet_deduplication_key: None,
 692            insert_text_mode: None,
 693            // This ensures that when a user accepts this completion, the
 694            // completion menu will still be shown after "@category " is
 695            // inserted
 696            confirm: Some(on_action),
 697        })
 698    }
 699
 700    fn completion_for_diagnostics(
 701        source_range: Range<Anchor>,
 702        source: Arc<T>,
 703        editor: WeakEntity<Editor>,
 704        mention_set: WeakEntity<MentionSet>,
 705        workspace: Entity<Workspace>,
 706        cx: &mut App,
 707    ) -> Vec<Completion> {
 708        let summary = workspace
 709            .read(cx)
 710            .project()
 711            .read(cx)
 712            .diagnostic_summary(false, cx);
 713        if summary.error_count == 0 && summary.warning_count == 0 {
 714            return Vec::new();
 715        }
 716        let icon_path = MentionUri::Diagnostics {
 717            include_errors: true,
 718            include_warnings: false,
 719        }
 720        .icon_path(cx);
 721
 722        let mut completions = Vec::new();
 723
 724        let cases = [
 725            (summary.error_count > 0, true, false),
 726            (summary.warning_count > 0, false, true),
 727            (
 728                summary.error_count > 0 && summary.warning_count > 0,
 729                true,
 730                true,
 731            ),
 732        ];
 733
 734        for (condition, include_errors, include_warnings) in cases {
 735            if condition {
 736                completions.push(Self::build_diagnostics_completion(
 737                    diagnostics_submenu_label(summary, include_errors, include_warnings),
 738                    source_range.clone(),
 739                    source.clone(),
 740                    editor.clone(),
 741                    mention_set.clone(),
 742                    workspace.clone(),
 743                    icon_path.clone(),
 744                    include_errors,
 745                    include_warnings,
 746                    summary,
 747                ));
 748            }
 749        }
 750
 751        completions
 752    }
 753
 754    fn build_diagnostics_completion(
 755        menu_label: String,
 756        source_range: Range<Anchor>,
 757        source: Arc<T>,
 758        editor: WeakEntity<Editor>,
 759        mention_set: WeakEntity<MentionSet>,
 760        workspace: Entity<Workspace>,
 761        icon_path: SharedString,
 762        include_errors: bool,
 763        include_warnings: bool,
 764        summary: DiagnosticSummary,
 765    ) -> Completion {
 766        let uri = MentionUri::Diagnostics {
 767            include_errors,
 768            include_warnings,
 769        };
 770        let crease_text = diagnostics_crease_label(summary, include_errors, include_warnings);
 771        let display_text = format!("@{}", crease_text);
 772        let new_text = format!("[{}]({}) ", display_text, uri.to_uri());
 773        let new_text_len = new_text.len();
 774        Completion {
 775            replace_range: source_range.clone(),
 776            new_text,
 777            label: CodeLabel::plain(menu_label, None),
 778            documentation: None,
 779            source: project::CompletionSource::Custom,
 780            icon_path: Some(icon_path),
 781            match_start: None,
 782            snippet_deduplication_key: None,
 783            insert_text_mode: None,
 784            confirm: Some(confirm_completion_callback(
 785                crease_text,
 786                source_range.start,
 787                new_text_len - 1,
 788                uri,
 789                source,
 790                editor,
 791                mention_set,
 792                workspace,
 793            )),
 794        }
 795    }
 796
 797    fn build_branch_diff_completion(
 798        base_ref: SharedString,
 799        source_range: Range<Anchor>,
 800        source: Arc<T>,
 801        editor: WeakEntity<Editor>,
 802        mention_set: WeakEntity<MentionSet>,
 803        workspace: Entity<Workspace>,
 804        cx: &mut App,
 805    ) -> Completion {
 806        let uri = MentionUri::GitDiff {
 807            base_ref: base_ref.to_string(),
 808        };
 809        let crease_text: SharedString = format!("Branch Diff (vs {})", base_ref).into();
 810        let display_text = format!("@{}", crease_text);
 811        let new_text = format!("[{}]({}) ", display_text, uri.to_uri());
 812        let new_text_len = new_text.len();
 813        let icon_path = uri.icon_path(cx);
 814
 815        Completion {
 816            replace_range: source_range.clone(),
 817            new_text,
 818            label: CodeLabel::plain(crease_text.to_string(), None),
 819            documentation: None,
 820            source: project::CompletionSource::Custom,
 821            icon_path: Some(icon_path),
 822            match_start: None,
 823            snippet_deduplication_key: None,
 824            insert_text_mode: None,
 825            confirm: Some(confirm_completion_callback(
 826                crease_text,
 827                source_range.start,
 828                new_text_len - 1,
 829                uri,
 830                source,
 831                editor,
 832                mention_set,
 833                workspace,
 834            )),
 835        }
 836    }
 837
 838    fn search_slash_commands(&self, query: String, cx: &mut App) -> Task<Vec<AvailableCommand>> {
 839        let commands = self.source.available_commands(cx);
 840        if commands.is_empty() {
 841            return Task::ready(Vec::new());
 842        }
 843
 844        cx.spawn(async move |cx| {
 845            let candidates = commands
 846                .iter()
 847                .enumerate()
 848                .map(|(id, command)| StringMatchCandidate::new(id, &command.name))
 849                .collect::<Vec<_>>();
 850
 851            let matches = fuzzy::match_strings(
 852                &candidates,
 853                &query,
 854                false,
 855                true,
 856                100,
 857                &Arc::new(AtomicBool::default()),
 858                cx.background_executor().clone(),
 859            )
 860            .await;
 861
 862            matches
 863                .into_iter()
 864                .map(|mat| commands[mat.candidate_id].clone())
 865                .collect()
 866        })
 867    }
 868
 869    fn fetch_branch_diff_match(
 870        &self,
 871        workspace: &Entity<Workspace>,
 872        cx: &mut App,
 873    ) -> Option<Task<Option<BranchDiffMatch>>> {
 874        let project = workspace.read(cx).project().clone();
 875        let repo = project.read(cx).active_repository(cx)?;
 876
 877        let default_branch_receiver = repo.update(cx, |repo, _| repo.default_branch(true));
 878
 879        Some(cx.spawn(async move |_cx| {
 880            let base_ref = default_branch_receiver
 881                .await
 882                .ok()
 883                .and_then(|r| r.ok())
 884                .flatten()?;
 885
 886            Some(BranchDiffMatch { base_ref })
 887        }))
 888    }
 889
 890    fn search_mentions(
 891        &self,
 892        mode: Option<PromptContextType>,
 893        query: String,
 894        cancellation_flag: Arc<AtomicBool>,
 895        cx: &mut App,
 896    ) -> Task<Vec<Match>> {
 897        let Some(workspace) = self.workspace.upgrade() else {
 898            return Task::ready(Vec::default());
 899        };
 900        match mode {
 901            Some(PromptContextType::File) => {
 902                let search_files_task = search_files(query, cancellation_flag, &workspace, cx);
 903                cx.background_spawn(async move {
 904                    search_files_task
 905                        .await
 906                        .into_iter()
 907                        .map(Match::File)
 908                        .collect()
 909                })
 910            }
 911
 912            Some(PromptContextType::Symbol) => {
 913                let search_symbols_task = search_symbols(query, cancellation_flag, &workspace, cx);
 914                cx.background_spawn(async move {
 915                    search_symbols_task
 916                        .await
 917                        .into_iter()
 918                        .map(Match::Symbol)
 919                        .collect()
 920                })
 921            }
 922
 923            Some(PromptContextType::Thread) => {
 924                if let Some(history) = self.history.as_ref().and_then(|h| h.upgrade()) {
 925                    let sessions = history
 926                        .read(cx)
 927                        .sessions()
 928                        .iter()
 929                        .map(|session| SessionMatch {
 930                            session_id: session.session_id.clone(),
 931                            title: session_title(session.title.clone()),
 932                        })
 933                        .collect::<Vec<_>>();
 934                    let search_task =
 935                        filter_sessions_by_query(query, cancellation_flag, sessions, cx);
 936                    cx.spawn(async move |_cx| {
 937                        search_task.await.into_iter().map(Match::Thread).collect()
 938                    })
 939                } else {
 940                    Task::ready(Vec::new())
 941                }
 942            }
 943
 944            Some(PromptContextType::Fetch) => {
 945                if !query.is_empty() {
 946                    Task::ready(vec![Match::Fetch(query.into())])
 947                } else {
 948                    Task::ready(Vec::new())
 949                }
 950            }
 951
 952            Some(PromptContextType::Rules) => {
 953                if let Some(prompt_store) = self.prompt_store.as_ref() {
 954                    let search_rules_task =
 955                        search_rules(query, cancellation_flag, prompt_store, cx);
 956                    cx.background_spawn(async move {
 957                        search_rules_task
 958                            .await
 959                            .into_iter()
 960                            .map(Match::Rules)
 961                            .collect::<Vec<_>>()
 962                    })
 963                } else {
 964                    Task::ready(Vec::new())
 965                }
 966            }
 967
 968            Some(PromptContextType::Diagnostics) => Task::ready(Vec::new()),
 969
 970            Some(PromptContextType::BranchDiff) => Task::ready(Vec::new()),
 971
 972            None if query.is_empty() => {
 973                let recent_task = self.recent_context_picker_entries(&workspace, cx);
 974                let entries = self
 975                    .available_context_picker_entries(&workspace, cx)
 976                    .into_iter()
 977                    .map(|mode| {
 978                        Match::Entry(EntryMatch {
 979                            entry: mode,
 980                            mat: None,
 981                        })
 982                    })
 983                    .collect::<Vec<_>>();
 984
 985                let branch_diff_task = if self
 986                    .source
 987                    .supports_context(PromptContextType::BranchDiff, cx)
 988                {
 989                    self.fetch_branch_diff_match(&workspace, cx)
 990                } else {
 991                    None
 992                };
 993
 994                cx.spawn(async move |_cx| {
 995                    let mut matches = recent_task.await;
 996                    matches.extend(entries);
 997
 998                    if let Some(branch_diff_task) = branch_diff_task {
 999                        if let Some(branch_diff_match) = branch_diff_task.await {
1000                            matches.push(Match::BranchDiff(branch_diff_match));
1001                        }
1002                    }
1003
1004                    matches
1005                })
1006            }
1007            None => {
1008                let executor = cx.background_executor().clone();
1009
1010                let search_files_task =
1011                    search_files(query.clone(), cancellation_flag, &workspace, cx);
1012
1013                let entries = self.available_context_picker_entries(&workspace, cx);
1014                let entry_candidates = entries
1015                    .iter()
1016                    .enumerate()
1017                    .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword()))
1018                    .collect::<Vec<_>>();
1019
1020                let branch_diff_task = if self
1021                    .source
1022                    .supports_context(PromptContextType::BranchDiff, cx)
1023                {
1024                    self.fetch_branch_diff_match(&workspace, cx)
1025                } else {
1026                    None
1027                };
1028
1029                cx.spawn(async move |cx| {
1030                    let mut matches = search_files_task
1031                        .await
1032                        .into_iter()
1033                        .map(Match::File)
1034                        .collect::<Vec<_>>();
1035
1036                    let entry_matches = fuzzy::match_strings(
1037                        &entry_candidates,
1038                        &query,
1039                        false,
1040                        true,
1041                        100,
1042                        &Arc::new(AtomicBool::default()),
1043                        executor,
1044                    )
1045                    .await;
1046
1047                    matches.extend(entry_matches.into_iter().map(|mat| {
1048                        Match::Entry(EntryMatch {
1049                            entry: entries[mat.candidate_id],
1050                            mat: Some(mat),
1051                        })
1052                    }));
1053
1054                    if let Some(branch_diff_task) = branch_diff_task {
1055                        let branch_diff_keyword = PromptContextType::BranchDiff.keyword();
1056                        let branch_diff_matches = fuzzy::match_strings(
1057                            &[StringMatchCandidate::new(0, branch_diff_keyword)],
1058                            &query,
1059                            false,
1060                            true,
1061                            1,
1062                            &Arc::new(AtomicBool::default()),
1063                            cx.background_executor().clone(),
1064                        )
1065                        .await;
1066
1067                        if !branch_diff_matches.is_empty() {
1068                            if let Some(branch_diff_match) = branch_diff_task.await {
1069                                matches.push(Match::BranchDiff(branch_diff_match));
1070                            }
1071                        }
1072                    }
1073
1074                    matches.sort_by(|a, b| {
1075                        b.score()
1076                            .partial_cmp(&a.score())
1077                            .unwrap_or(std::cmp::Ordering::Equal)
1078                    });
1079
1080                    matches
1081                })
1082            }
1083        }
1084    }
1085
1086    fn recent_context_picker_entries(
1087        &self,
1088        workspace: &Entity<Workspace>,
1089        cx: &mut App,
1090    ) -> Task<Vec<Match>> {
1091        let mut recent = Vec::with_capacity(6);
1092
1093        let mut mentions = self
1094            .mention_set
1095            .read_with(cx, |store, _cx| store.mentions());
1096        let workspace = workspace.read(cx);
1097        let project = workspace.project().read(cx);
1098        let include_root_name = workspace.visible_worktrees(cx).count() > 1;
1099
1100        if let Some(agent_panel) = workspace.panel::<AgentPanel>(cx)
1101            && let Some(thread) = agent_panel.read(cx).active_agent_thread(cx)
1102            && let Some(title) = thread.read(cx).title()
1103        {
1104            mentions.insert(MentionUri::Thread {
1105                id: thread.read(cx).session_id().clone(),
1106                name: title.to_string(),
1107            });
1108        }
1109
1110        recent.extend(
1111            workspace
1112                .recent_navigation_history_iter(cx)
1113                .filter(|(_, abs_path)| {
1114                    abs_path.as_ref().is_none_or(|path| {
1115                        !mentions.contains(&MentionUri::File {
1116                            abs_path: path.clone(),
1117                        })
1118                    })
1119                })
1120                .take(4)
1121                .filter_map(|(project_path, _)| {
1122                    project
1123                        .worktree_for_id(project_path.worktree_id, cx)
1124                        .map(|worktree| {
1125                            let path_prefix = if include_root_name {
1126                                worktree.read(cx).root_name().into()
1127                            } else {
1128                                RelPath::empty().into()
1129                            };
1130                            Match::File(FileMatch {
1131                                mat: fuzzy::PathMatch {
1132                                    score: 1.,
1133                                    positions: Vec::new(),
1134                                    worktree_id: project_path.worktree_id.to_usize(),
1135                                    path: project_path.path,
1136                                    path_prefix,
1137                                    is_dir: false,
1138                                    distance_to_relative_ancestor: 0,
1139                                },
1140                                is_recent: true,
1141                            })
1142                        })
1143                }),
1144        );
1145
1146        if !self.source.supports_context(PromptContextType::Thread, cx) {
1147            return Task::ready(recent);
1148        }
1149
1150        if let Some(history) = self.history.as_ref().and_then(|h| h.upgrade()) {
1151            const RECENT_COUNT: usize = 2;
1152            recent.extend(
1153                history
1154                    .read(cx)
1155                    .sessions()
1156                    .into_iter()
1157                    .map(|session| SessionMatch {
1158                        session_id: session.session_id.clone(),
1159                        title: session_title(session.title.clone()),
1160                    })
1161                    .filter(|session| {
1162                        let uri = MentionUri::Thread {
1163                            id: session.session_id.clone(),
1164                            name: session.title.to_string(),
1165                        };
1166                        !mentions.contains(&uri)
1167                    })
1168                    .take(RECENT_COUNT)
1169                    .map(Match::RecentThread),
1170            );
1171            return Task::ready(recent);
1172        }
1173
1174        Task::ready(recent)
1175    }
1176
1177    fn available_context_picker_entries(
1178        &self,
1179        workspace: &Entity<Workspace>,
1180        cx: &mut App,
1181    ) -> Vec<PromptContextEntry> {
1182        let mut entries = vec![
1183            PromptContextEntry::Mode(PromptContextType::File),
1184            PromptContextEntry::Mode(PromptContextType::Symbol),
1185        ];
1186
1187        if self.source.supports_context(PromptContextType::Thread, cx) {
1188            entries.push(PromptContextEntry::Mode(PromptContextType::Thread));
1189        }
1190
1191        let has_editor_selection = workspace
1192            .read(cx)
1193            .active_item(cx)
1194            .and_then(|item| item.downcast::<Editor>())
1195            .is_some_and(|editor| {
1196                editor.update(cx, |editor, cx| {
1197                    editor.has_non_empty_selection(&editor.display_snapshot(cx))
1198                })
1199            });
1200
1201        let has_terminal_selection = !terminal_selections_if_panel_open(workspace, cx).is_empty();
1202
1203        if has_editor_selection || has_terminal_selection {
1204            entries.push(PromptContextEntry::Action(
1205                PromptContextAction::AddSelections,
1206            ));
1207        }
1208
1209        if self.prompt_store.is_some() && self.source.supports_context(PromptContextType::Rules, cx)
1210        {
1211            entries.push(PromptContextEntry::Mode(PromptContextType::Rules));
1212        }
1213
1214        if self.source.supports_context(PromptContextType::Fetch, cx) {
1215            entries.push(PromptContextEntry::Mode(PromptContextType::Fetch));
1216        }
1217
1218        if self
1219            .source
1220            .supports_context(PromptContextType::Diagnostics, cx)
1221        {
1222            let summary = workspace
1223                .read(cx)
1224                .project()
1225                .read(cx)
1226                .diagnostic_summary(false, cx);
1227            if summary.error_count > 0 || summary.warning_count > 0 {
1228                entries.push(PromptContextEntry::Mode(PromptContextType::Diagnostics));
1229            }
1230        }
1231
1232        entries
1233    }
1234}
1235
1236impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletionProvider<T> {
1237    fn completions(
1238        &self,
1239        _excerpt_id: ExcerptId,
1240        buffer: &Entity<Buffer>,
1241        buffer_position: Anchor,
1242        _trigger: CompletionContext,
1243        window: &mut Window,
1244        cx: &mut Context<Editor>,
1245    ) -> Task<Result<Vec<CompletionResponse>>> {
1246        let state = buffer.update(cx, |buffer, cx| {
1247            let position = buffer_position.to_point(buffer);
1248            let line_start = Point::new(position.row, 0);
1249            let offset_to_line = buffer.point_to_offset(line_start);
1250            let mut lines = buffer.text_for_range(line_start..position).lines();
1251            let line = lines.next()?;
1252            PromptCompletion::try_parse(line, offset_to_line, &self.source.supported_modes(cx))
1253        });
1254        let Some(state) = state else {
1255            return Task::ready(Ok(Vec::new()));
1256        };
1257
1258        let Some(workspace) = self.workspace.upgrade() else {
1259            return Task::ready(Ok(Vec::new()));
1260        };
1261
1262        let project = workspace.read(cx).project().clone();
1263        let snapshot = buffer.read(cx).snapshot();
1264        let source_range = snapshot.anchor_before(state.source_range().start)
1265            ..snapshot.anchor_after(state.source_range().end);
1266
1267        let source = self.source.clone();
1268        let editor = self.editor.clone();
1269        let mention_set = self.mention_set.downgrade();
1270        match state {
1271            PromptCompletion::SlashCommand(SlashCommandCompletion {
1272                command, argument, ..
1273            }) => {
1274                let search_task = self.search_slash_commands(command.unwrap_or_default(), cx);
1275                cx.background_spawn(async move {
1276                    let completions = search_task
1277                        .await
1278                        .into_iter()
1279                        .map(|command| {
1280                            let new_text = if let Some(argument) = argument.as_ref() {
1281                                format!("/{} {}", command.name, argument)
1282                            } else {
1283                                format!("/{} ", command.name)
1284                            };
1285
1286                            let is_missing_argument =
1287                                command.requires_argument && argument.is_none();
1288
1289                            Completion {
1290                                replace_range: source_range.clone(),
1291                                new_text,
1292                                label: CodeLabel::plain(command.name.to_string(), None),
1293                                documentation: Some(CompletionDocumentation::MultiLinePlainText(
1294                                    command.description.into(),
1295                                )),
1296                                source: project::CompletionSource::Custom,
1297                                icon_path: None,
1298                                match_start: None,
1299                                snippet_deduplication_key: None,
1300                                insert_text_mode: None,
1301                                confirm: Some(Arc::new({
1302                                    let source = source.clone();
1303                                    move |intent, _window, cx| {
1304                                        if !is_missing_argument {
1305                                            cx.defer({
1306                                                let source = source.clone();
1307                                                move |cx| match intent {
1308                                                    CompletionIntent::Complete
1309                                                    | CompletionIntent::CompleteWithInsert
1310                                                    | CompletionIntent::CompleteWithReplace => {
1311                                                        source.confirm_command(cx);
1312                                                    }
1313                                                    CompletionIntent::Compose => {}
1314                                                }
1315                                            });
1316                                        }
1317                                        false
1318                                    }
1319                                })),
1320                            }
1321                        })
1322                        .collect();
1323
1324                    Ok(vec![CompletionResponse {
1325                        completions,
1326                        display_options: CompletionDisplayOptions {
1327                            dynamic_width: true,
1328                        },
1329                        // Since this does its own filtering (see `filter_completions()` returns false),
1330                        // there is no benefit to computing whether this set of completions is incomplete.
1331                        is_incomplete: true,
1332                    }])
1333                })
1334            }
1335            PromptCompletion::Mention(MentionCompletion { mode, argument, .. }) => {
1336                if let Some(PromptContextType::Diagnostics) = mode {
1337                    if argument.is_some() {
1338                        return Task::ready(Ok(Vec::new()));
1339                    }
1340
1341                    let completions = Self::completion_for_diagnostics(
1342                        source_range.clone(),
1343                        source.clone(),
1344                        editor.clone(),
1345                        mention_set.clone(),
1346                        workspace.clone(),
1347                        cx,
1348                    );
1349                    if !completions.is_empty() {
1350                        return Task::ready(Ok(vec![CompletionResponse {
1351                            completions,
1352                            display_options: CompletionDisplayOptions::default(),
1353                            is_incomplete: false,
1354                        }]));
1355                    }
1356                }
1357
1358                let query = argument.unwrap_or_default();
1359                let search_task =
1360                    self.search_mentions(mode, query, Arc::<AtomicBool>::default(), cx);
1361
1362                // Calculate maximum characters available for the full label (file_name + space + directory)
1363                // based on maximum menu width after accounting for padding, spacing, and icon width
1364                let label_max_chars = {
1365                    // Base06 left padding + Base06 gap + Base06 right padding + icon width
1366                    let used_pixels = DynamicSpacing::Base06.px(cx) * 3.0
1367                        + IconSize::XSmall.rems() * window.rem_size();
1368
1369                    let style = window.text_style();
1370                    let font_id = window.text_system().resolve_font(&style.font());
1371                    let font_size = TextSize::Small.rems(cx).to_pixels(window.rem_size());
1372
1373                    // Fallback em_width of 10px matches file_finder.rs fallback for TextSize::Small
1374                    let em_width = cx
1375                        .text_system()
1376                        .em_width(font_id, font_size)
1377                        .unwrap_or(px(10.0));
1378
1379                    // Calculate available pixels for text (file_name + directory)
1380                    // Using max width since dynamic_width allows the menu to expand up to this
1381                    let available_pixels = COMPLETION_MENU_MAX_WIDTH - used_pixels;
1382
1383                    // Convert to character count (total available for file_name + directory)
1384                    (f32::from(available_pixels) / f32::from(em_width)) as usize
1385                };
1386
1387                cx.spawn(async move |_, cx| {
1388                    let matches = search_task.await;
1389
1390                    let completions = cx.update(|cx| {
1391                        matches
1392                            .into_iter()
1393                            .filter_map(|mat| match mat {
1394                                Match::File(FileMatch { mat, is_recent }) => {
1395                                    let project_path = ProjectPath {
1396                                        worktree_id: WorktreeId::from_usize(mat.worktree_id),
1397                                        path: mat.path.clone(),
1398                                    };
1399
1400                                    // If path is empty, this means we're matching with the root directory itself
1401                                    // so we use the path_prefix as the name
1402                                    let path_prefix = if mat.path.is_empty() {
1403                                        project
1404                                            .read(cx)
1405                                            .worktree_for_id(project_path.worktree_id, cx)
1406                                            .map(|wt| wt.read(cx).root_name().into())
1407                                            .unwrap_or_else(|| mat.path_prefix.clone())
1408                                    } else {
1409                                        mat.path_prefix.clone()
1410                                    };
1411
1412                                    Self::completion_for_path(
1413                                        project_path,
1414                                        &path_prefix,
1415                                        is_recent,
1416                                        mat.is_dir,
1417                                        source_range.clone(),
1418                                        source.clone(),
1419                                        editor.clone(),
1420                                        mention_set.clone(),
1421                                        workspace.clone(),
1422                                        project.clone(),
1423                                        label_max_chars,
1424                                        cx,
1425                                    )
1426                                }
1427                                Match::Symbol(SymbolMatch { symbol, .. }) => {
1428                                    Self::completion_for_symbol(
1429                                        symbol,
1430                                        source_range.clone(),
1431                                        source.clone(),
1432                                        editor.clone(),
1433                                        mention_set.clone(),
1434                                        workspace.clone(),
1435                                        label_max_chars,
1436                                        cx,
1437                                    )
1438                                }
1439                                Match::Thread(thread) => Some(Self::completion_for_thread(
1440                                    thread.session_id,
1441                                    Some(thread.title),
1442                                    source_range.clone(),
1443                                    false,
1444                                    source.clone(),
1445                                    editor.clone(),
1446                                    mention_set.clone(),
1447                                    workspace.clone(),
1448                                    cx,
1449                                )),
1450                                Match::RecentThread(thread) => Some(Self::completion_for_thread(
1451                                    thread.session_id,
1452                                    Some(thread.title),
1453                                    source_range.clone(),
1454                                    true,
1455                                    source.clone(),
1456                                    editor.clone(),
1457                                    mention_set.clone(),
1458                                    workspace.clone(),
1459                                    cx,
1460                                )),
1461                                Match::Rules(user_rules) => Some(Self::completion_for_rules(
1462                                    user_rules,
1463                                    source_range.clone(),
1464                                    source.clone(),
1465                                    editor.clone(),
1466                                    mention_set.clone(),
1467                                    workspace.clone(),
1468                                    cx,
1469                                )),
1470                                Match::Fetch(url) => Self::completion_for_fetch(
1471                                    source_range.clone(),
1472                                    url,
1473                                    source.clone(),
1474                                    editor.clone(),
1475                                    mention_set.clone(),
1476                                    workspace.clone(),
1477                                    cx,
1478                                ),
1479                                Match::Entry(EntryMatch { entry, .. }) => {
1480                                    Self::completion_for_entry(
1481                                        entry,
1482                                        source_range.clone(),
1483                                        editor.clone(),
1484                                        mention_set.clone(),
1485                                        &workspace,
1486                                        cx,
1487                                    )
1488                                }
1489                                Match::BranchDiff(branch_diff) => {
1490                                    Some(Self::build_branch_diff_completion(
1491                                        branch_diff.base_ref,
1492                                        source_range.clone(),
1493                                        source.clone(),
1494                                        editor.clone(),
1495                                        mention_set.clone(),
1496                                        workspace.clone(),
1497                                        cx,
1498                                    ))
1499                                }
1500                            })
1501                            .collect::<Vec<_>>()
1502                    });
1503
1504                    Ok(vec![CompletionResponse {
1505                        completions,
1506                        display_options: CompletionDisplayOptions {
1507                            dynamic_width: true,
1508                        },
1509                        // Since this does its own filtering (see `filter_completions()` returns false),
1510                        // there is no benefit to computing whether this set of completions is incomplete.
1511                        is_incomplete: true,
1512                    }])
1513                })
1514            }
1515        }
1516    }
1517
1518    fn is_completion_trigger(
1519        &self,
1520        buffer: &Entity<language::Buffer>,
1521        position: language::Anchor,
1522        _text: &str,
1523        _trigger_in_words: bool,
1524        cx: &mut Context<Editor>,
1525    ) -> bool {
1526        let buffer = buffer.read(cx);
1527        let position = position.to_point(buffer);
1528        let line_start = Point::new(position.row, 0);
1529        let offset_to_line = buffer.point_to_offset(line_start);
1530        let mut lines = buffer.text_for_range(line_start..position).lines();
1531        if let Some(line) = lines.next() {
1532            PromptCompletion::try_parse(line, offset_to_line, &self.source.supported_modes(cx))
1533                .filter(|completion| {
1534                    // Right now we don't support completing arguments of slash commands
1535                    let is_slash_command_with_argument = matches!(
1536                        completion,
1537                        PromptCompletion::SlashCommand(SlashCommandCompletion {
1538                            argument: Some(_),
1539                            ..
1540                        })
1541                    );
1542                    !is_slash_command_with_argument
1543                })
1544                .map(|completion| {
1545                    completion.source_range().start <= offset_to_line + position.column as usize
1546                        && completion.source_range().end
1547                            >= offset_to_line + position.column as usize
1548                })
1549                .unwrap_or(false)
1550        } else {
1551            false
1552        }
1553    }
1554
1555    fn sort_completions(&self) -> bool {
1556        false
1557    }
1558
1559    fn filter_completions(&self) -> bool {
1560        false
1561    }
1562}
1563
1564fn confirm_completion_callback<T: PromptCompletionProviderDelegate>(
1565    crease_text: SharedString,
1566    start: Anchor,
1567    content_len: usize,
1568    mention_uri: MentionUri,
1569    source: Arc<T>,
1570    editor: WeakEntity<Editor>,
1571    mention_set: WeakEntity<MentionSet>,
1572    workspace: Entity<Workspace>,
1573) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
1574    Arc::new(move |_, window, cx| {
1575        let source = source.clone();
1576        let editor = editor.clone();
1577        let mention_set = mention_set.clone();
1578        let crease_text = crease_text.clone();
1579        let mention_uri = mention_uri.clone();
1580        let workspace = workspace.clone();
1581        window.defer(cx, move |window, cx| {
1582            if let Some(editor) = editor.upgrade() {
1583                mention_set
1584                    .clone()
1585                    .update(cx, |mention_set, cx| {
1586                        mention_set
1587                            .confirm_mention_completion(
1588                                crease_text,
1589                                start,
1590                                content_len,
1591                                mention_uri,
1592                                source.supports_images(cx),
1593                                editor,
1594                                &workspace,
1595                                window,
1596                                cx,
1597                            )
1598                            .detach();
1599                    })
1600                    .ok();
1601            }
1602        });
1603        false
1604    })
1605}
1606
1607#[derive(Debug, PartialEq)]
1608enum PromptCompletion {
1609    SlashCommand(SlashCommandCompletion),
1610    Mention(MentionCompletion),
1611}
1612
1613impl PromptCompletion {
1614    fn source_range(&self) -> Range<usize> {
1615        match self {
1616            Self::SlashCommand(completion) => completion.source_range.clone(),
1617            Self::Mention(completion) => completion.source_range.clone(),
1618        }
1619    }
1620
1621    fn try_parse(
1622        line: &str,
1623        offset_to_line: usize,
1624        supported_modes: &[PromptContextType],
1625    ) -> Option<Self> {
1626        if line.contains('@') {
1627            if let Some(mention) =
1628                MentionCompletion::try_parse(line, offset_to_line, supported_modes)
1629            {
1630                return Some(Self::Mention(mention));
1631            }
1632        }
1633        SlashCommandCompletion::try_parse(line, offset_to_line).map(Self::SlashCommand)
1634    }
1635}
1636
1637#[derive(Debug, Default, PartialEq)]
1638pub struct SlashCommandCompletion {
1639    pub source_range: Range<usize>,
1640    pub command: Option<String>,
1641    pub argument: Option<String>,
1642}
1643
1644impl SlashCommandCompletion {
1645    pub fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
1646        // If we decide to support commands that are not at the beginning of the prompt, we can remove this check
1647        if !line.starts_with('/') || offset_to_line != 0 {
1648            return None;
1649        }
1650
1651        let (prefix, last_command) = line.rsplit_once('/')?;
1652        if prefix.chars().last().is_some_and(|c| !c.is_whitespace())
1653            || last_command.starts_with(char::is_whitespace)
1654        {
1655            return None;
1656        }
1657
1658        let mut argument = None;
1659        let mut command = None;
1660        if let Some((command_text, args)) = last_command.split_once(char::is_whitespace) {
1661            if !args.is_empty() {
1662                argument = Some(args.trim_end().to_string());
1663            }
1664            command = Some(command_text.to_string());
1665        } else if !last_command.is_empty() {
1666            command = Some(last_command.to_string());
1667        };
1668
1669        Some(Self {
1670            source_range: prefix.len() + offset_to_line
1671                ..line
1672                    .rfind(|c: char| !c.is_whitespace())
1673                    .unwrap_or_else(|| line.len())
1674                    + 1
1675                    + offset_to_line,
1676            command,
1677            argument,
1678        })
1679    }
1680}
1681
1682#[derive(Debug, Default, PartialEq)]
1683struct MentionCompletion {
1684    source_range: Range<usize>,
1685    mode: Option<PromptContextType>,
1686    argument: Option<String>,
1687}
1688
1689impl MentionCompletion {
1690    fn try_parse(
1691        line: &str,
1692        offset_to_line: usize,
1693        supported_modes: &[PromptContextType],
1694    ) -> Option<Self> {
1695        // Find the rightmost '@' that has a word boundary before it and no whitespace immediately after
1696        let mut last_mention_start = None;
1697        for (idx, _) in line.rmatch_indices('@') {
1698            // No whitespace immediately after '@'
1699            if line[idx + 1..]
1700                .chars()
1701                .next()
1702                .is_some_and(|c| c.is_whitespace())
1703            {
1704                continue;
1705            }
1706
1707            // Must be a word boundary before '@'
1708            if idx > 0
1709                && line[..idx]
1710                    .chars()
1711                    .last()
1712                    .is_some_and(|c| !c.is_whitespace())
1713            {
1714                continue;
1715            }
1716
1717            last_mention_start = Some(idx);
1718            break;
1719        }
1720
1721        let last_mention_start = last_mention_start?;
1722
1723        let rest_of_line = &line[last_mention_start + 1..];
1724
1725        let mut mode = None;
1726        let mut argument = None;
1727
1728        let mut parts = rest_of_line.split_whitespace();
1729        let mut end = last_mention_start + 1;
1730
1731        if let Some(mode_text) = parts.next() {
1732            // Safe since we check no leading whitespace above
1733            end += mode_text.len();
1734
1735            if let Some(parsed_mode) = PromptContextType::try_from(mode_text).ok()
1736                && supported_modes.contains(&parsed_mode)
1737            {
1738                mode = Some(parsed_mode);
1739            } else {
1740                argument = Some(mode_text.to_string());
1741            }
1742            match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
1743                Some(whitespace_count) => {
1744                    if let Some(argument_text) = parts.next() {
1745                        // If mode wasn't recognized but we have an argument, don't suggest completions
1746                        // (e.g. '@something word')
1747                        if mode.is_none() && !argument_text.is_empty() {
1748                            return None;
1749                        }
1750
1751                        argument = Some(argument_text.to_string());
1752                        end += whitespace_count + argument_text.len();
1753                    }
1754                }
1755                None => {
1756                    // Rest of line is entirely whitespace
1757                    end += rest_of_line.len() - mode_text.len();
1758                }
1759            }
1760        }
1761
1762        Some(Self {
1763            source_range: last_mention_start + offset_to_line..end + offset_to_line,
1764            mode,
1765            argument,
1766        })
1767    }
1768}
1769
1770fn diagnostics_label(
1771    summary: DiagnosticSummary,
1772    include_errors: bool,
1773    include_warnings: bool,
1774) -> String {
1775    let mut parts = Vec::new();
1776
1777    if include_errors && summary.error_count > 0 {
1778        parts.push(format!(
1779            "{} {}",
1780            summary.error_count,
1781            pluralize("error", summary.error_count)
1782        ));
1783    }
1784
1785    if include_warnings && summary.warning_count > 0 {
1786        parts.push(format!(
1787            "{} {}",
1788            summary.warning_count,
1789            pluralize("warning", summary.warning_count)
1790        ));
1791    }
1792
1793    if parts.is_empty() {
1794        return "Diagnostics".into();
1795    }
1796
1797    let body = if parts.len() == 2 {
1798        format!("{} and {}", parts[0], parts[1])
1799    } else {
1800        parts
1801            .pop()
1802            .expect("at least one part present after non-empty check")
1803    };
1804
1805    format!("Diagnostics: {body}")
1806}
1807
1808fn diagnostics_submenu_label(
1809    summary: DiagnosticSummary,
1810    include_errors: bool,
1811    include_warnings: bool,
1812) -> String {
1813    match (include_errors, include_warnings) {
1814        (true, true) => format!(
1815            "{} {} & {} {}",
1816            summary.error_count,
1817            pluralize("error", summary.error_count),
1818            summary.warning_count,
1819            pluralize("warning", summary.warning_count)
1820        ),
1821        (true, _) => format!(
1822            "{} {}",
1823            summary.error_count,
1824            pluralize("error", summary.error_count)
1825        ),
1826        (_, true) => format!(
1827            "{} {}",
1828            summary.warning_count,
1829            pluralize("warning", summary.warning_count)
1830        ),
1831        _ => "Diagnostics".into(),
1832    }
1833}
1834
1835fn diagnostics_crease_label(
1836    summary: DiagnosticSummary,
1837    include_errors: bool,
1838    include_warnings: bool,
1839) -> SharedString {
1840    diagnostics_label(summary, include_errors, include_warnings).into()
1841}
1842
1843fn pluralize(noun: &str, count: usize) -> String {
1844    if count == 1 {
1845        noun.to_string()
1846    } else {
1847        format!("{noun}s")
1848    }
1849}
1850
1851pub(crate) fn search_files(
1852    query: String,
1853    cancellation_flag: Arc<AtomicBool>,
1854    workspace: &Entity<Workspace>,
1855    cx: &App,
1856) -> Task<Vec<FileMatch>> {
1857    if query.is_empty() {
1858        let workspace = workspace.read(cx);
1859        let project = workspace.project().read(cx);
1860        let visible_worktrees = workspace.visible_worktrees(cx).collect::<Vec<_>>();
1861        let include_root_name = visible_worktrees.len() > 1;
1862
1863        let recent_matches = workspace
1864            .recent_navigation_history(Some(10), cx)
1865            .into_iter()
1866            .map(|(project_path, _)| {
1867                let path_prefix = if include_root_name {
1868                    project
1869                        .worktree_for_id(project_path.worktree_id, cx)
1870                        .map(|wt| wt.read(cx).root_name().into())
1871                        .unwrap_or_else(|| RelPath::empty().into())
1872                } else {
1873                    RelPath::empty().into()
1874                };
1875
1876                FileMatch {
1877                    mat: PathMatch {
1878                        score: 0.,
1879                        positions: Vec::new(),
1880                        worktree_id: project_path.worktree_id.to_usize(),
1881                        path: project_path.path,
1882                        path_prefix,
1883                        distance_to_relative_ancestor: 0,
1884                        is_dir: false,
1885                    },
1886                    is_recent: true,
1887                }
1888            });
1889
1890        let file_matches = visible_worktrees.into_iter().flat_map(|worktree| {
1891            let worktree = worktree.read(cx);
1892            let path_prefix: Arc<RelPath> = if include_root_name {
1893                worktree.root_name().into()
1894            } else {
1895                RelPath::empty().into()
1896            };
1897            worktree.entries(false, 0).map(move |entry| FileMatch {
1898                mat: PathMatch {
1899                    score: 0.,
1900                    positions: Vec::new(),
1901                    worktree_id: worktree.id().to_usize(),
1902                    path: entry.path.clone(),
1903                    path_prefix: path_prefix.clone(),
1904                    distance_to_relative_ancestor: 0,
1905                    is_dir: entry.is_dir(),
1906                },
1907                is_recent: false,
1908            })
1909        });
1910
1911        Task::ready(recent_matches.chain(file_matches).collect())
1912    } else {
1913        let workspace = workspace.read(cx);
1914        let relative_to = workspace
1915            .recent_navigation_history_iter(cx)
1916            .next()
1917            .map(|(path, _)| path.path);
1918        let worktrees = workspace.visible_worktrees(cx).collect::<Vec<_>>();
1919        let include_root_name = worktrees.len() > 1;
1920        let candidate_sets = worktrees
1921            .into_iter()
1922            .map(|worktree| {
1923                let worktree = worktree.read(cx);
1924
1925                PathMatchCandidateSet {
1926                    snapshot: worktree.snapshot(),
1927                    include_ignored: worktree.root_entry().is_some_and(|entry| entry.is_ignored),
1928                    include_root_name,
1929                    candidates: project::Candidates::Entries,
1930                }
1931            })
1932            .collect::<Vec<_>>();
1933
1934        let executor = cx.background_executor().clone();
1935        cx.foreground_executor().spawn(async move {
1936            fuzzy::match_path_sets(
1937                candidate_sets.as_slice(),
1938                query.as_str(),
1939                &relative_to,
1940                false,
1941                100,
1942                &cancellation_flag,
1943                executor,
1944            )
1945            .await
1946            .into_iter()
1947            .map(|mat| FileMatch {
1948                mat,
1949                is_recent: false,
1950            })
1951            .collect::<Vec<_>>()
1952        })
1953    }
1954}
1955
1956pub(crate) fn search_symbols(
1957    query: String,
1958    cancellation_flag: Arc<AtomicBool>,
1959    workspace: &Entity<Workspace>,
1960    cx: &mut App,
1961) -> Task<Vec<SymbolMatch>> {
1962    let symbols_task = workspace.update(cx, |workspace, cx| {
1963        workspace
1964            .project()
1965            .update(cx, |project, cx| project.symbols(&query, cx))
1966    });
1967    let project = workspace.read(cx).project().clone();
1968    cx.spawn(async move |cx| {
1969        let Some(symbols) = symbols_task.await.log_err() else {
1970            return Vec::new();
1971        };
1972        let (visible_match_candidates, external_match_candidates): (Vec<_>, Vec<_>) = project
1973            .update(cx, |project, cx| {
1974                symbols
1975                    .iter()
1976                    .enumerate()
1977                    .map(|(id, symbol)| StringMatchCandidate::new(id, symbol.label.filter_text()))
1978                    .partition(|candidate| match &symbols[candidate.id].path {
1979                        SymbolLocation::InProject(project_path) => project
1980                            .entry_for_path(project_path, cx)
1981                            .is_some_and(|e| !e.is_ignored),
1982                        SymbolLocation::OutsideProject { .. } => false,
1983                    })
1984            });
1985        // Try to support rust-analyzer's path based symbols feature which
1986        // allows to search by rust path syntax, in that case we only want to
1987        // filter names by the last segment
1988        // Ideally this was a first class LSP feature (rich queries)
1989        let query = query
1990            .rsplit_once("::")
1991            .map_or(&*query, |(_, suffix)| suffix)
1992            .to_owned();
1993        // Note if you make changes to this filtering below, also change `project_symbols::ProjectSymbolsDelegate::filter`
1994        const MAX_MATCHES: usize = 100;
1995        let mut visible_matches = cx.foreground_executor().block_on(fuzzy::match_strings(
1996            &visible_match_candidates,
1997            &query,
1998            false,
1999            true,
2000            MAX_MATCHES,
2001            &cancellation_flag,
2002            cx.background_executor().clone(),
2003        ));
2004        let mut external_matches = cx.foreground_executor().block_on(fuzzy::match_strings(
2005            &external_match_candidates,
2006            &query,
2007            false,
2008            true,
2009            MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
2010            &cancellation_flag,
2011            cx.background_executor().clone(),
2012        ));
2013        let sort_key_for_match = |mat: &StringMatch| {
2014            let symbol = &symbols[mat.candidate_id];
2015            (Reverse(OrderedFloat(mat.score)), symbol.label.filter_text())
2016        };
2017
2018        visible_matches.sort_unstable_by_key(sort_key_for_match);
2019        external_matches.sort_unstable_by_key(sort_key_for_match);
2020        let mut matches = visible_matches;
2021        matches.append(&mut external_matches);
2022
2023        matches
2024            .into_iter()
2025            .map(|mut mat| {
2026                let symbol = symbols[mat.candidate_id].clone();
2027                let filter_start = symbol.label.filter_range.start;
2028                for position in &mut mat.positions {
2029                    *position += filter_start;
2030                }
2031                SymbolMatch { symbol }
2032            })
2033            .collect()
2034    })
2035}
2036
2037fn filter_sessions_by_query(
2038    query: String,
2039    cancellation_flag: Arc<AtomicBool>,
2040    sessions: Vec<SessionMatch>,
2041    cx: &mut App,
2042) -> Task<Vec<SessionMatch>> {
2043    if query.is_empty() {
2044        return Task::ready(sessions);
2045    }
2046    let executor = cx.background_executor().clone();
2047    cx.background_spawn(async move {
2048        filter_sessions(query, cancellation_flag, sessions, executor).await
2049    })
2050}
2051
2052async fn filter_sessions(
2053    query: String,
2054    cancellation_flag: Arc<AtomicBool>,
2055    sessions: Vec<SessionMatch>,
2056    executor: BackgroundExecutor,
2057) -> Vec<SessionMatch> {
2058    let titles = sessions
2059        .iter()
2060        .map(|session| session.title.clone())
2061        .collect::<Vec<_>>();
2062    let candidates = titles
2063        .iter()
2064        .enumerate()
2065        .map(|(id, title)| StringMatchCandidate::new(id, title.as_ref()))
2066        .collect::<Vec<_>>();
2067    let matches = fuzzy::match_strings(
2068        &candidates,
2069        &query,
2070        false,
2071        true,
2072        100,
2073        &cancellation_flag,
2074        executor,
2075    )
2076    .await;
2077
2078    matches
2079        .into_iter()
2080        .map(|mat| sessions[mat.candidate_id].clone())
2081        .collect()
2082}
2083
2084pub(crate) fn search_rules(
2085    query: String,
2086    cancellation_flag: Arc<AtomicBool>,
2087    prompt_store: &Entity<PromptStore>,
2088    cx: &mut App,
2089) -> Task<Vec<RulesContextEntry>> {
2090    let search_task = prompt_store.read(cx).search(query, cancellation_flag, cx);
2091    cx.background_spawn(async move {
2092        search_task
2093            .await
2094            .into_iter()
2095            .flat_map(|metadata| {
2096                // Default prompts are filtered out as they are automatically included.
2097                if metadata.default {
2098                    None
2099                } else {
2100                    Some(RulesContextEntry {
2101                        prompt_id: metadata.id.as_user()?,
2102                        title: metadata.title?,
2103                    })
2104                }
2105            })
2106            .collect::<Vec<_>>()
2107    })
2108}
2109
2110pub struct SymbolMatch {
2111    pub symbol: Symbol,
2112}
2113
2114pub struct FileMatch {
2115    pub mat: PathMatch,
2116    pub is_recent: bool,
2117}
2118
2119pub fn extract_file_name_and_directory(
2120    path: &RelPath,
2121    path_prefix: &RelPath,
2122    path_style: PathStyle,
2123) -> (SharedString, Option<SharedString>) {
2124    // If path is empty, this means we're matching with the root directory itself
2125    // so we use the path_prefix as the name
2126    if path.is_empty() && !path_prefix.is_empty() {
2127        return (path_prefix.display(path_style).to_string().into(), None);
2128    }
2129
2130    let full_path = path_prefix.join(path);
2131    let file_name = full_path.file_name().unwrap_or_default();
2132    let display_path = full_path.display(path_style);
2133    let (directory, file_name) = display_path.split_at(display_path.len() - file_name.len());
2134    (
2135        file_name.to_string().into(),
2136        Some(SharedString::new(directory)).filter(|dir| !dir.is_empty()),
2137    )
2138}
2139
2140fn build_code_label_for_path(
2141    file: &str,
2142    directory: Option<&str>,
2143    line_number: Option<u32>,
2144    label_max_chars: usize,
2145    cx: &App,
2146) -> CodeLabel {
2147    let variable_highlight_id = cx
2148        .theme()
2149        .syntax()
2150        .highlight_id("variable")
2151        .map(HighlightId);
2152    let mut label = CodeLabelBuilder::default();
2153
2154    label.push_str(file, None);
2155    label.push_str(" ", None);
2156
2157    if let Some(directory) = directory {
2158        let file_name_chars = file.chars().count();
2159        // Account for: file_name + space (ellipsis is handled by truncate_and_remove_front)
2160        let directory_max_chars = label_max_chars
2161            .saturating_sub(file_name_chars)
2162            .saturating_sub(1);
2163        let truncated_directory = truncate_and_remove_front(directory, directory_max_chars.max(5));
2164        label.push_str(&truncated_directory, variable_highlight_id);
2165    }
2166    if let Some(line_number) = line_number {
2167        label.push_str(&format!(" L{}", line_number), variable_highlight_id);
2168    }
2169    label.build()
2170}
2171
2172/// Returns terminal selections from all terminal views if the terminal panel is open.
2173fn terminal_selections_if_panel_open(workspace: &Entity<Workspace>, cx: &App) -> Vec<String> {
2174    let Some(panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
2175        return Vec::new();
2176    };
2177
2178    // Check if the dock containing this panel is open
2179    let position = match TerminalSettings::get_global(cx).dock {
2180        TerminalDockPosition::Left => DockPosition::Left,
2181        TerminalDockPosition::Bottom => DockPosition::Bottom,
2182        TerminalDockPosition::Right => DockPosition::Right,
2183    };
2184    let dock_is_open = workspace
2185        .read(cx)
2186        .dock_at_position(position)
2187        .read(cx)
2188        .is_open();
2189    if !dock_is_open {
2190        return Vec::new();
2191    }
2192
2193    panel.read(cx).terminal_selections(cx)
2194}
2195
2196fn selection_ranges(
2197    workspace: &Entity<Workspace>,
2198    cx: &mut App,
2199) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
2200    let Some(editor) = workspace
2201        .read(cx)
2202        .active_item(cx)
2203        .and_then(|item| item.act_as::<Editor>(cx))
2204    else {
2205        return Vec::new();
2206    };
2207
2208    editor.update(cx, |editor, cx| {
2209        let selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
2210
2211        let buffer = editor.buffer().clone().read(cx);
2212        let snapshot = buffer.snapshot(cx);
2213
2214        selections
2215            .into_iter()
2216            .map(|s| {
2217                let (start, end) = if s.is_empty() {
2218                    let row = multi_buffer::MultiBufferRow(s.start.row);
2219                    let line_start = text::Point::new(s.start.row, 0);
2220                    let line_end = text::Point::new(s.start.row, snapshot.line_len(row));
2221                    (line_start, line_end)
2222                } else {
2223                    (s.start, s.end)
2224                };
2225                snapshot.anchor_after(start)..snapshot.anchor_before(end)
2226            })
2227            .flat_map(|range| {
2228                let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
2229                let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
2230                if start_buffer != end_buffer {
2231                    return None;
2232                }
2233                Some((start_buffer, start..end))
2234            })
2235            .collect::<Vec<_>>()
2236    })
2237}
2238
2239#[cfg(test)]
2240mod tests {
2241    use super::*;
2242    use gpui::TestAppContext;
2243
2244    #[test]
2245    fn test_prompt_completion_parse() {
2246        let supported_modes = vec![PromptContextType::File, PromptContextType::Symbol];
2247
2248        assert_eq!(
2249            PromptCompletion::try_parse("/", 0, &supported_modes),
2250            Some(PromptCompletion::SlashCommand(SlashCommandCompletion {
2251                source_range: 0..1,
2252                command: None,
2253                argument: None,
2254            }))
2255        );
2256
2257        assert_eq!(
2258            PromptCompletion::try_parse("@", 0, &supported_modes),
2259            Some(PromptCompletion::Mention(MentionCompletion {
2260                source_range: 0..1,
2261                mode: None,
2262                argument: None,
2263            }))
2264        );
2265
2266        assert_eq!(
2267            PromptCompletion::try_parse("/test @file", 0, &supported_modes),
2268            Some(PromptCompletion::Mention(MentionCompletion {
2269                source_range: 6..11,
2270                mode: Some(PromptContextType::File),
2271                argument: None,
2272            }))
2273        );
2274    }
2275
2276    #[test]
2277    fn test_slash_command_completion_parse() {
2278        assert_eq!(
2279            SlashCommandCompletion::try_parse("/", 0),
2280            Some(SlashCommandCompletion {
2281                source_range: 0..1,
2282                command: None,
2283                argument: None,
2284            })
2285        );
2286
2287        assert_eq!(
2288            SlashCommandCompletion::try_parse("/help", 0),
2289            Some(SlashCommandCompletion {
2290                source_range: 0..5,
2291                command: Some("help".to_string()),
2292                argument: None,
2293            })
2294        );
2295
2296        assert_eq!(
2297            SlashCommandCompletion::try_parse("/help ", 0),
2298            Some(SlashCommandCompletion {
2299                source_range: 0..5,
2300                command: Some("help".to_string()),
2301                argument: None,
2302            })
2303        );
2304
2305        assert_eq!(
2306            SlashCommandCompletion::try_parse("/help arg1", 0),
2307            Some(SlashCommandCompletion {
2308                source_range: 0..10,
2309                command: Some("help".to_string()),
2310                argument: Some("arg1".to_string()),
2311            })
2312        );
2313
2314        assert_eq!(
2315            SlashCommandCompletion::try_parse("/help arg1 arg2", 0),
2316            Some(SlashCommandCompletion {
2317                source_range: 0..15,
2318                command: Some("help".to_string()),
2319                argument: Some("arg1 arg2".to_string()),
2320            })
2321        );
2322
2323        assert_eq!(
2324            SlashCommandCompletion::try_parse("/拿不到命令 拿不到命令 ", 0),
2325            Some(SlashCommandCompletion {
2326                source_range: 0..30,
2327                command: Some("拿不到命令".to_string()),
2328                argument: Some("拿不到命令".to_string()),
2329            })
2330        );
2331
2332        assert_eq!(SlashCommandCompletion::try_parse("Lorem Ipsum", 0), None);
2333
2334        assert_eq!(SlashCommandCompletion::try_parse("Lorem /", 0), None);
2335
2336        assert_eq!(SlashCommandCompletion::try_parse("Lorem /help", 0), None);
2337
2338        assert_eq!(SlashCommandCompletion::try_parse("Lorem/", 0), None);
2339
2340        assert_eq!(SlashCommandCompletion::try_parse("/ ", 0), None);
2341    }
2342
2343    #[test]
2344    fn test_mention_completion_parse() {
2345        let supported_modes = vec![PromptContextType::File, PromptContextType::Symbol];
2346        let supported_modes_with_diagnostics = vec![
2347            PromptContextType::File,
2348            PromptContextType::Symbol,
2349            PromptContextType::Diagnostics,
2350        ];
2351
2352        assert_eq!(
2353            MentionCompletion::try_parse("Lorem Ipsum", 0, &supported_modes),
2354            None
2355        );
2356
2357        assert_eq!(
2358            MentionCompletion::try_parse("Lorem @", 0, &supported_modes),
2359            Some(MentionCompletion {
2360                source_range: 6..7,
2361                mode: None,
2362                argument: None,
2363            })
2364        );
2365
2366        assert_eq!(
2367            MentionCompletion::try_parse("Lorem @file", 0, &supported_modes),
2368            Some(MentionCompletion {
2369                source_range: 6..11,
2370                mode: Some(PromptContextType::File),
2371                argument: None,
2372            })
2373        );
2374
2375        assert_eq!(
2376            MentionCompletion::try_parse("Lorem @file ", 0, &supported_modes),
2377            Some(MentionCompletion {
2378                source_range: 6..12,
2379                mode: Some(PromptContextType::File),
2380                argument: None,
2381            })
2382        );
2383
2384        assert_eq!(
2385            MentionCompletion::try_parse("Lorem @file main.rs", 0, &supported_modes),
2386            Some(MentionCompletion {
2387                source_range: 6..19,
2388                mode: Some(PromptContextType::File),
2389                argument: Some("main.rs".to_string()),
2390            })
2391        );
2392
2393        assert_eq!(
2394            MentionCompletion::try_parse("Lorem @file main.rs ", 0, &supported_modes),
2395            Some(MentionCompletion {
2396                source_range: 6..19,
2397                mode: Some(PromptContextType::File),
2398                argument: Some("main.rs".to_string()),
2399            })
2400        );
2401
2402        assert_eq!(
2403            MentionCompletion::try_parse("Lorem @file main.rs Ipsum", 0, &supported_modes),
2404            Some(MentionCompletion {
2405                source_range: 6..19,
2406                mode: Some(PromptContextType::File),
2407                argument: Some("main.rs".to_string()),
2408            })
2409        );
2410
2411        assert_eq!(
2412            MentionCompletion::try_parse("Lorem @main", 0, &supported_modes),
2413            Some(MentionCompletion {
2414                source_range: 6..11,
2415                mode: None,
2416                argument: Some("main".to_string()),
2417            })
2418        );
2419
2420        assert_eq!(
2421            MentionCompletion::try_parse("Lorem @main ", 0, &supported_modes),
2422            Some(MentionCompletion {
2423                source_range: 6..12,
2424                mode: None,
2425                argument: Some("main".to_string()),
2426            })
2427        );
2428
2429        assert_eq!(
2430            MentionCompletion::try_parse("Lorem @main m", 0, &supported_modes),
2431            None
2432        );
2433
2434        assert_eq!(
2435            MentionCompletion::try_parse("test@", 0, &supported_modes),
2436            None
2437        );
2438
2439        // Allowed non-file mentions
2440
2441        assert_eq!(
2442            MentionCompletion::try_parse("Lorem @symbol main", 0, &supported_modes),
2443            Some(MentionCompletion {
2444                source_range: 6..18,
2445                mode: Some(PromptContextType::Symbol),
2446                argument: Some("main".to_string()),
2447            })
2448        );
2449
2450        assert_eq!(
2451            MentionCompletion::try_parse(
2452                "Lorem @symbol agent_ui::completion_provider",
2453                0,
2454                &supported_modes
2455            ),
2456            Some(MentionCompletion {
2457                source_range: 6..43,
2458                mode: Some(PromptContextType::Symbol),
2459                argument: Some("agent_ui::completion_provider".to_string()),
2460            })
2461        );
2462
2463        assert_eq!(
2464            MentionCompletion::try_parse(
2465                "Lorem @diagnostics",
2466                0,
2467                &supported_modes_with_diagnostics
2468            ),
2469            Some(MentionCompletion {
2470                source_range: 6..18,
2471                mode: Some(PromptContextType::Diagnostics),
2472                argument: None,
2473            })
2474        );
2475
2476        // Disallowed non-file mentions
2477        assert_eq!(
2478            MentionCompletion::try_parse("Lorem @symbol main", 0, &[PromptContextType::File]),
2479            None
2480        );
2481
2482        assert_eq!(
2483            MentionCompletion::try_parse("Lorem@symbol", 0, &supported_modes),
2484            None,
2485            "Should not parse mention inside word"
2486        );
2487
2488        assert_eq!(
2489            MentionCompletion::try_parse("Lorem @ file", 0, &supported_modes),
2490            None,
2491            "Should not parse with a space after @"
2492        );
2493
2494        assert_eq!(
2495            MentionCompletion::try_parse("@ file", 0, &supported_modes),
2496            None,
2497            "Should not parse with a space after @ at the start of the line"
2498        );
2499
2500        assert_eq!(
2501            MentionCompletion::try_parse(
2502                "@fetch https://www.npmjs.com/package/@matterport/sdk",
2503                0,
2504                &[PromptContextType::Fetch]
2505            ),
2506            Some(MentionCompletion {
2507                source_range: 0..52,
2508                mode: Some(PromptContextType::Fetch),
2509                argument: Some("https://www.npmjs.com/package/@matterport/sdk".to_string()),
2510            }),
2511            "Should handle URLs with @ in the path"
2512        );
2513
2514        assert_eq!(
2515            MentionCompletion::try_parse(
2516                "@fetch https://example.com/@org/@repo/file",
2517                0,
2518                &[PromptContextType::Fetch]
2519            ),
2520            Some(MentionCompletion {
2521                source_range: 0..42,
2522                mode: Some(PromptContextType::Fetch),
2523                argument: Some("https://example.com/@org/@repo/file".to_string()),
2524            }),
2525            "Should handle URLs with multiple @ characters"
2526        );
2527
2528        assert_eq!(
2529            MentionCompletion::try_parse(
2530                "@fetch https://example.com/@",
2531                0,
2532                &[PromptContextType::Fetch]
2533            ),
2534            Some(MentionCompletion {
2535                source_range: 0..28,
2536                mode: Some(PromptContextType::Fetch),
2537                argument: Some("https://example.com/@".to_string()),
2538            }),
2539            "Should parse URL ending with @ (even if URL is incomplete)"
2540        );
2541    }
2542
2543    #[gpui::test]
2544    async fn test_filter_sessions_by_query(cx: &mut TestAppContext) {
2545        let alpha = SessionMatch {
2546            session_id: acp::SessionId::new("session-alpha"),
2547            title: "Alpha Session".into(),
2548        };
2549        let beta = SessionMatch {
2550            session_id: acp::SessionId::new("session-beta"),
2551            title: "Beta Session".into(),
2552        };
2553
2554        let sessions = vec![alpha.clone(), beta];
2555
2556        let task = {
2557            let mut app = cx.app.borrow_mut();
2558            filter_sessions_by_query(
2559                "Alpha".into(),
2560                Arc::new(AtomicBool::default()),
2561                sessions,
2562                &mut app,
2563            )
2564        };
2565
2566        let results = task.await;
2567        assert_eq!(results.len(), 1);
2568        assert_eq!(results[0].session_id, alpha.session_id);
2569    }
2570
2571    #[gpui::test]
2572    async fn test_search_files_path_distance_ordering(cx: &mut TestAppContext) {
2573        use project::Project;
2574        use serde_json::json;
2575        use util::{path, rel_path::rel_path};
2576        use workspace::{AppState, MultiWorkspace};
2577
2578        let app_state = cx.update(|cx| {
2579            let state = AppState::test(cx);
2580            theme::init(theme::LoadThemes::JustBase, cx);
2581            editor::init(cx);
2582            state
2583        });
2584
2585        app_state
2586            .fs
2587            .as_fake()
2588            .insert_tree(
2589                path!("/root"),
2590                json!({
2591                    "dir1": { "a.txt": "" },
2592                    "dir2": {
2593                        "a.txt": "",
2594                        "b.txt": ""
2595                    }
2596                }),
2597            )
2598            .await;
2599
2600        let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2601        let (multi_workspace, cx) =
2602            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
2603        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2604
2605        let worktree_id = cx.read(|cx| {
2606            let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
2607            assert_eq!(worktrees.len(), 1);
2608            worktrees[0].read(cx).id()
2609        });
2610
2611        // Open a file in dir2 to create navigation history.
2612        // When searching for "a.txt", dir2/a.txt should be sorted first because
2613        // it is closer to the most recently opened file (dir2/b.txt).
2614        let b_path = ProjectPath {
2615            worktree_id,
2616            path: rel_path("dir2/b.txt").into(),
2617        };
2618        workspace
2619            .update_in(cx, |workspace, window, cx| {
2620                workspace.open_path(b_path, None, true, window, cx)
2621            })
2622            .await
2623            .unwrap();
2624
2625        let results = cx
2626            .update(|_window, cx| {
2627                search_files(
2628                    "a.txt".into(),
2629                    Arc::new(AtomicBool::default()),
2630                    &workspace,
2631                    cx,
2632                )
2633            })
2634            .await;
2635
2636        assert_eq!(results.len(), 2, "expected 2 matching files");
2637        assert_eq!(
2638            results[0].mat.path.as_ref(),
2639            rel_path("dir2/a.txt"),
2640            "dir2/a.txt should be first because it's closer to the recently opened dir2/b.txt"
2641        );
2642        assert_eq!(
2643            results[1].mat.path.as_ref(),
2644            rel_path("dir1/a.txt"),
2645            "dir1/a.txt should be second"
2646        );
2647    }
2648}