completion_provider.rs

   1use std::cell::RefCell;
   2use std::ops::Range;
   3use std::path::{Path, PathBuf};
   4use std::rc::Rc;
   5use std::sync::Arc;
   6use std::sync::atomic::AtomicBool;
   7
   8use anyhow::Result;
   9use editor::{CompletionProvider, Editor, ExcerptId, ToOffset as _};
  10use file_icons::FileIcons;
  11use fuzzy::{StringMatch, StringMatchCandidate};
  12use gpui::{App, Entity, Task, WeakEntity};
  13use http_client::HttpClientWithUrl;
  14use itertools::Itertools;
  15use language::{Buffer, CodeLabel, HighlightId};
  16use lsp::CompletionContext;
  17use project::{Completion, CompletionIntent, ProjectPath, Symbol, WorktreeId};
  18use prompt_store::PromptId;
  19use rope::Point;
  20use text::{Anchor, OffsetRangeExt, ToPoint};
  21use ui::prelude::*;
  22use workspace::Workspace;
  23
  24use crate::context::RULES_ICON;
  25use crate::context_picker::file_context_picker::search_files;
  26use crate::context_picker::symbol_context_picker::search_symbols;
  27use crate::context_store::ContextStore;
  28use crate::thread_store::ThreadStore;
  29
  30use super::fetch_context_picker::fetch_url_content;
  31use super::file_context_picker::FileMatch;
  32use super::rules_context_picker::{RulesContextEntry, search_rules};
  33use super::symbol_context_picker::SymbolMatch;
  34use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
  35use super::{
  36    ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry,
  37    available_context_picker_entries, recent_context_picker_entries, selection_ranges,
  38};
  39
  40pub(crate) enum Match {
  41    Symbol(SymbolMatch),
  42    File(FileMatch),
  43    Thread(ThreadMatch),
  44    Fetch(SharedString),
  45    Rules(RulesContextEntry),
  46    Entry(EntryMatch),
  47}
  48
  49pub struct EntryMatch {
  50    mat: Option<StringMatch>,
  51    entry: ContextPickerEntry,
  52}
  53
  54impl Match {
  55    pub fn score(&self) -> f64 {
  56        match self {
  57            Match::File(file) => file.mat.score,
  58            Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
  59            Match::Thread(_) => 1.,
  60            Match::Symbol(_) => 1.,
  61            Match::Fetch(_) => 1.,
  62            Match::Rules(_) => 1.,
  63        }
  64    }
  65}
  66
  67fn search(
  68    mode: Option<ContextPickerMode>,
  69    query: String,
  70    cancellation_flag: Arc<AtomicBool>,
  71    recent_entries: Vec<RecentEntry>,
  72    thread_store: Option<WeakEntity<ThreadStore>>,
  73    workspace: Entity<Workspace>,
  74    cx: &mut App,
  75) -> Task<Vec<Match>> {
  76    match mode {
  77        Some(ContextPickerMode::File) => {
  78            let search_files_task =
  79                search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
  80            cx.background_spawn(async move {
  81                search_files_task
  82                    .await
  83                    .into_iter()
  84                    .map(Match::File)
  85                    .collect()
  86            })
  87        }
  88        Some(ContextPickerMode::Symbol) => {
  89            let search_symbols_task =
  90                search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx);
  91            cx.background_spawn(async move {
  92                search_symbols_task
  93                    .await
  94                    .into_iter()
  95                    .map(Match::Symbol)
  96                    .collect()
  97            })
  98        }
  99        Some(ContextPickerMode::Thread) => {
 100            if let Some(thread_store) = thread_store.as_ref().and_then(|t| t.upgrade()) {
 101                let search_threads_task =
 102                    search_threads(query.clone(), cancellation_flag.clone(), thread_store, cx);
 103                cx.background_spawn(async move {
 104                    search_threads_task
 105                        .await
 106                        .into_iter()
 107                        .map(Match::Thread)
 108                        .collect()
 109                })
 110            } else {
 111                Task::ready(Vec::new())
 112            }
 113        }
 114        Some(ContextPickerMode::Fetch) => {
 115            if !query.is_empty() {
 116                Task::ready(vec![Match::Fetch(query.into())])
 117            } else {
 118                Task::ready(Vec::new())
 119            }
 120        }
 121        Some(ContextPickerMode::Rules) => {
 122            if let Some(thread_store) = thread_store.as_ref().and_then(|t| t.upgrade()) {
 123                let search_rules_task =
 124                    search_rules(query.clone(), cancellation_flag.clone(), thread_store, cx);
 125                cx.background_spawn(async move {
 126                    search_rules_task
 127                        .await
 128                        .into_iter()
 129                        .map(Match::Rules)
 130                        .collect::<Vec<_>>()
 131                })
 132            } else {
 133                Task::ready(Vec::new())
 134            }
 135        }
 136        None => {
 137            if query.is_empty() {
 138                let mut matches = recent_entries
 139                    .into_iter()
 140                    .map(|entry| match entry {
 141                        super::RecentEntry::File {
 142                            project_path,
 143                            path_prefix,
 144                        } => Match::File(FileMatch {
 145                            mat: fuzzy::PathMatch {
 146                                score: 1.,
 147                                positions: Vec::new(),
 148                                worktree_id: project_path.worktree_id.to_usize(),
 149                                path: project_path.path,
 150                                path_prefix,
 151                                is_dir: false,
 152                                distance_to_relative_ancestor: 0,
 153                            },
 154                            is_recent: true,
 155                        }),
 156                        super::RecentEntry::Thread(thread_context_entry) => {
 157                            Match::Thread(ThreadMatch {
 158                                thread: thread_context_entry,
 159                                is_recent: true,
 160                            })
 161                        }
 162                    })
 163                    .collect::<Vec<_>>();
 164
 165                matches.extend(
 166                    available_context_picker_entries(&thread_store, &workspace, cx)
 167                        .into_iter()
 168                        .map(|mode| {
 169                            Match::Entry(EntryMatch {
 170                                entry: mode,
 171                                mat: None,
 172                            })
 173                        }),
 174                );
 175
 176                Task::ready(matches)
 177            } else {
 178                let executor = cx.background_executor().clone();
 179
 180                let search_files_task =
 181                    search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
 182
 183                let entries = available_context_picker_entries(&thread_store, &workspace, cx);
 184                let entry_candidates = entries
 185                    .iter()
 186                    .enumerate()
 187                    .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword()))
 188                    .collect::<Vec<_>>();
 189
 190                cx.background_spawn(async move {
 191                    let mut matches = search_files_task
 192                        .await
 193                        .into_iter()
 194                        .map(Match::File)
 195                        .collect::<Vec<_>>();
 196
 197                    let entry_matches = fuzzy::match_strings(
 198                        &entry_candidates,
 199                        &query,
 200                        false,
 201                        100,
 202                        &Arc::new(AtomicBool::default()),
 203                        executor,
 204                    )
 205                    .await;
 206
 207                    matches.extend(entry_matches.into_iter().map(|mat| {
 208                        Match::Entry(EntryMatch {
 209                            entry: entries[mat.candidate_id],
 210                            mat: Some(mat),
 211                        })
 212                    }));
 213
 214                    matches.sort_by(|a, b| {
 215                        b.score()
 216                            .partial_cmp(&a.score())
 217                            .unwrap_or(std::cmp::Ordering::Equal)
 218                    });
 219
 220                    matches
 221                })
 222            }
 223        }
 224    }
 225}
 226
 227pub struct ContextPickerCompletionProvider {
 228    workspace: WeakEntity<Workspace>,
 229    context_store: WeakEntity<ContextStore>,
 230    thread_store: Option<WeakEntity<ThreadStore>>,
 231    editor: WeakEntity<Editor>,
 232}
 233
 234impl ContextPickerCompletionProvider {
 235    pub fn new(
 236        workspace: WeakEntity<Workspace>,
 237        context_store: WeakEntity<ContextStore>,
 238        thread_store: Option<WeakEntity<ThreadStore>>,
 239        editor: WeakEntity<Editor>,
 240    ) -> Self {
 241        Self {
 242            workspace,
 243            context_store,
 244            thread_store,
 245            editor,
 246        }
 247    }
 248
 249    fn completion_for_entry(
 250        entry: ContextPickerEntry,
 251        excerpt_id: ExcerptId,
 252        source_range: Range<Anchor>,
 253        editor: Entity<Editor>,
 254        context_store: Entity<ContextStore>,
 255        workspace: &Entity<Workspace>,
 256        cx: &mut App,
 257    ) -> Option<Completion> {
 258        match entry {
 259            ContextPickerEntry::Mode(mode) => Some(Completion {
 260                replace_range: source_range.clone(),
 261                new_text: format!("@{} ", mode.keyword()),
 262                label: CodeLabel::plain(mode.label().to_string(), None),
 263                icon_path: Some(mode.icon().path().into()),
 264                documentation: None,
 265                source: project::CompletionSource::Custom,
 266                insert_text_mode: None,
 267                // This ensures that when a user accepts this completion, the
 268                // completion menu will still be shown after "@category " is
 269                // inserted
 270                confirm: Some(Arc::new(|_, _, _| true)),
 271            }),
 272            ContextPickerEntry::Action(action) => {
 273                let (new_text, on_action) = match action {
 274                    ContextPickerAction::AddSelections => {
 275                        let selections = selection_ranges(workspace, cx);
 276
 277                        let selection_infos = selections
 278                            .iter()
 279                            .map(|(buffer, range)| {
 280                                let full_path = buffer
 281                                    .read(cx)
 282                                    .file()
 283                                    .map(|file| file.full_path(cx))
 284                                    .unwrap_or_else(|| PathBuf::from("untitled"));
 285                                let file_name = full_path
 286                                    .file_name()
 287                                    .unwrap_or_default()
 288                                    .to_string_lossy()
 289                                    .to_string();
 290                                let line_range = range.to_point(&buffer.read(cx).snapshot());
 291
 292                                let link = MentionLink::for_selection(
 293                                    &file_name,
 294                                    &full_path.to_string_lossy(),
 295                                    line_range.start.row as usize..line_range.end.row as usize,
 296                                );
 297                                (file_name, link, line_range)
 298                            })
 299                            .collect::<Vec<_>>();
 300
 301                        let new_text = selection_infos.iter().map(|(_, link, _)| link).join(" ");
 302
 303                        let callback = Arc::new({
 304                            let context_store = context_store.clone();
 305                            let selections = selections.clone();
 306                            let selection_infos = selection_infos.clone();
 307                            move |_, _: &mut Window, cx: &mut App| {
 308                                context_store.update(cx, |context_store, cx| {
 309                                    for (buffer, range) in &selections {
 310                                        context_store
 311                                            .add_selection(buffer.clone(), range.clone(), cx)
 312                                            .detach_and_log_err(cx)
 313                                    }
 314                                });
 315
 316                                let editor = editor.clone();
 317                                let selection_infos = selection_infos.clone();
 318                                cx.defer(move |cx| {
 319                                    let mut current_offset = 0;
 320                                    for (file_name, link, line_range) in selection_infos.iter() {
 321                                        let snapshot =
 322                                            editor.read(cx).buffer().read(cx).snapshot(cx);
 323                                        let Some(start) = snapshot
 324                                            .anchor_in_excerpt(excerpt_id, source_range.start)
 325                                        else {
 326                                            return;
 327                                        };
 328
 329                                        let offset = start.to_offset(&snapshot) + current_offset;
 330                                        let text_len = link.len();
 331
 332                                        let range = snapshot.anchor_after(offset)
 333                                            ..snapshot.anchor_after(offset + text_len);
 334
 335                                        let crease = super::crease_for_mention(
 336                                            format!(
 337                                                "{} ({}-{})",
 338                                                file_name,
 339                                                line_range.start.row + 1,
 340                                                line_range.end.row + 1
 341                                            )
 342                                            .into(),
 343                                            IconName::Context.path().into(),
 344                                            range,
 345                                            editor.downgrade(),
 346                                        );
 347
 348                                        editor.update(cx, |editor, cx| {
 349                                            editor.display_map.update(cx, |display_map, cx| {
 350                                                display_map.fold(vec![crease], cx);
 351                                            });
 352                                        });
 353
 354                                        current_offset += text_len + 1;
 355                                    }
 356                                });
 357
 358                                false
 359                            }
 360                        });
 361
 362                        (new_text, callback)
 363                    }
 364                };
 365
 366                Some(Completion {
 367                    replace_range: source_range.clone(),
 368                    new_text,
 369                    label: CodeLabel::plain(action.label().to_string(), None),
 370                    icon_path: Some(action.icon().path().into()),
 371                    documentation: None,
 372                    source: project::CompletionSource::Custom,
 373                    insert_text_mode: None,
 374                    // This ensures that when a user accepts this completion, the
 375                    // completion menu will still be shown after "@category " is
 376                    // inserted
 377                    confirm: Some(on_action),
 378                })
 379            }
 380        }
 381    }
 382
 383    fn completion_for_thread(
 384        thread_entry: ThreadContextEntry,
 385        excerpt_id: ExcerptId,
 386        source_range: Range<Anchor>,
 387        recent: bool,
 388        editor: Entity<Editor>,
 389        context_store: Entity<ContextStore>,
 390        thread_store: Entity<ThreadStore>,
 391    ) -> Completion {
 392        let icon_for_completion = if recent {
 393            IconName::HistoryRerun
 394        } else {
 395            IconName::MessageBubbles
 396        };
 397        let new_text = MentionLink::for_thread(&thread_entry);
 398        let new_text_len = new_text.len();
 399        Completion {
 400            replace_range: source_range.clone(),
 401            new_text,
 402            label: CodeLabel::plain(thread_entry.summary.to_string(), None),
 403            documentation: None,
 404            insert_text_mode: None,
 405            source: project::CompletionSource::Custom,
 406            icon_path: Some(icon_for_completion.path().into()),
 407            confirm: Some(confirm_completion_callback(
 408                IconName::MessageBubbles.path().into(),
 409                thread_entry.summary.clone(),
 410                excerpt_id,
 411                source_range.start,
 412                new_text_len,
 413                editor.clone(),
 414                move |cx| {
 415                    let thread_id = thread_entry.id.clone();
 416                    let context_store = context_store.clone();
 417                    let thread_store = thread_store.clone();
 418                    cx.spawn(async move |cx| {
 419                        let thread = thread_store
 420                            .update(cx, |thread_store, cx| {
 421                                thread_store.open_thread(&thread_id, cx)
 422                            })?
 423                            .await?;
 424                        context_store.update(cx, |context_store, cx| {
 425                            context_store.add_thread(thread, false, cx)
 426                        })
 427                    })
 428                    .detach_and_log_err(cx);
 429                },
 430            )),
 431        }
 432    }
 433
 434    fn completion_for_rules(
 435        rules: RulesContextEntry,
 436        excerpt_id: ExcerptId,
 437        source_range: Range<Anchor>,
 438        editor: Entity<Editor>,
 439        context_store: Entity<ContextStore>,
 440        thread_store: Entity<ThreadStore>,
 441    ) -> Completion {
 442        let new_text = MentionLink::for_rules(&rules);
 443        let new_text_len = new_text.len();
 444        Completion {
 445            replace_range: source_range.clone(),
 446            new_text,
 447            label: CodeLabel::plain(rules.title.to_string(), None),
 448            documentation: None,
 449            insert_text_mode: None,
 450            source: project::CompletionSource::Custom,
 451            icon_path: Some(RULES_ICON.path().into()),
 452            confirm: Some(confirm_completion_callback(
 453                RULES_ICON.path().into(),
 454                rules.title.clone(),
 455                excerpt_id,
 456                source_range.start,
 457                new_text_len,
 458                editor.clone(),
 459                move |cx| {
 460                    let prompt_uuid = rules.prompt_id;
 461                    let prompt_id = PromptId::User { uuid: prompt_uuid };
 462                    let context_store = context_store.clone();
 463                    let Some(prompt_store) = thread_store.read(cx).prompt_store() else {
 464                        log::error!("Can't add user rules as prompt store is missing.");
 465                        return;
 466                    };
 467                    let prompt_store = prompt_store.read(cx);
 468                    let Some(metadata) = prompt_store.metadata(prompt_id) else {
 469                        return;
 470                    };
 471                    let Some(title) = metadata.title else {
 472                        return;
 473                    };
 474                    let text_task = prompt_store.load(prompt_id, cx);
 475
 476                    cx.spawn(async move |cx| {
 477                        let text = text_task.await?;
 478                        context_store.update(cx, |context_store, cx| {
 479                            context_store.add_rules(prompt_uuid, title, text, false, cx)
 480                        })
 481                    })
 482                    .detach_and_log_err(cx);
 483                },
 484            )),
 485        }
 486    }
 487
 488    fn completion_for_fetch(
 489        source_range: Range<Anchor>,
 490        url_to_fetch: SharedString,
 491        excerpt_id: ExcerptId,
 492        editor: Entity<Editor>,
 493        context_store: Entity<ContextStore>,
 494        http_client: Arc<HttpClientWithUrl>,
 495    ) -> Completion {
 496        let new_text = MentionLink::for_fetch(&url_to_fetch);
 497        let new_text_len = new_text.len();
 498        Completion {
 499            replace_range: source_range.clone(),
 500            new_text,
 501            label: CodeLabel::plain(url_to_fetch.to_string(), None),
 502            documentation: None,
 503            source: project::CompletionSource::Custom,
 504            icon_path: Some(IconName::Globe.path().into()),
 505            insert_text_mode: None,
 506            confirm: Some(confirm_completion_callback(
 507                IconName::Globe.path().into(),
 508                url_to_fetch.clone(),
 509                excerpt_id,
 510                source_range.start,
 511                new_text_len,
 512                editor.clone(),
 513                move |cx| {
 514                    let context_store = context_store.clone();
 515                    let http_client = http_client.clone();
 516                    let url_to_fetch = url_to_fetch.clone();
 517                    cx.spawn(async move |cx| {
 518                        if context_store.update(cx, |context_store, _| {
 519                            context_store.includes_url(&url_to_fetch).is_some()
 520                        })? {
 521                            return Ok(());
 522                        }
 523                        let content = cx
 524                            .background_spawn(fetch_url_content(
 525                                http_client,
 526                                url_to_fetch.to_string(),
 527                            ))
 528                            .await?;
 529                        context_store.update(cx, |context_store, cx| {
 530                            context_store.add_fetched_url(url_to_fetch.to_string(), content, cx)
 531                        })
 532                    })
 533                    .detach_and_log_err(cx);
 534                },
 535            )),
 536        }
 537    }
 538
 539    fn completion_for_path(
 540        project_path: ProjectPath,
 541        path_prefix: &str,
 542        is_recent: bool,
 543        is_directory: bool,
 544        excerpt_id: ExcerptId,
 545        source_range: Range<Anchor>,
 546        editor: Entity<Editor>,
 547        context_store: Entity<ContextStore>,
 548        cx: &App,
 549    ) -> Completion {
 550        let (file_name, directory) = super::file_context_picker::extract_file_name_and_directory(
 551            &project_path.path,
 552            path_prefix,
 553        );
 554
 555        let label =
 556            build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
 557        let full_path = if let Some(directory) = directory {
 558            format!("{}{}", directory, file_name)
 559        } else {
 560            file_name.to_string()
 561        };
 562
 563        let crease_icon_path = if is_directory {
 564            FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into())
 565        } else {
 566            FileIcons::get_icon(Path::new(&full_path), cx)
 567                .unwrap_or_else(|| IconName::File.path().into())
 568        };
 569        let completion_icon_path = if is_recent {
 570            IconName::HistoryRerun.path().into()
 571        } else {
 572            crease_icon_path.clone()
 573        };
 574
 575        let new_text = MentionLink::for_file(&file_name, &full_path);
 576        let new_text_len = new_text.len();
 577        Completion {
 578            replace_range: source_range.clone(),
 579            new_text,
 580            label,
 581            documentation: None,
 582            source: project::CompletionSource::Custom,
 583            icon_path: Some(completion_icon_path),
 584            insert_text_mode: None,
 585            confirm: Some(confirm_completion_callback(
 586                crease_icon_path,
 587                file_name,
 588                excerpt_id,
 589                source_range.start,
 590                new_text_len,
 591                editor,
 592                move |cx| {
 593                    context_store.update(cx, |context_store, cx| {
 594                        let task = if is_directory {
 595                            context_store.add_directory(project_path.clone(), false, cx)
 596                        } else {
 597                            context_store.add_file_from_path(project_path.clone(), false, cx)
 598                        };
 599                        task.detach_and_log_err(cx);
 600                    })
 601                },
 602            )),
 603        }
 604    }
 605
 606    fn completion_for_symbol(
 607        symbol: Symbol,
 608        excerpt_id: ExcerptId,
 609        source_range: Range<Anchor>,
 610        editor: Entity<Editor>,
 611        context_store: Entity<ContextStore>,
 612        workspace: Entity<Workspace>,
 613        cx: &mut App,
 614    ) -> Option<Completion> {
 615        let path_prefix = workspace
 616            .read(cx)
 617            .project()
 618            .read(cx)
 619            .worktree_for_id(symbol.path.worktree_id, cx)?
 620            .read(cx)
 621            .root_name();
 622
 623        let (file_name, directory) = super::file_context_picker::extract_file_name_and_directory(
 624            &symbol.path.path,
 625            path_prefix,
 626        );
 627        let full_path = if let Some(directory) = directory {
 628            format!("{}{}", directory, file_name)
 629        } else {
 630            file_name.to_string()
 631        };
 632
 633        let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
 634        let mut label = CodeLabel::plain(symbol.name.clone(), None);
 635        label.push_str(" ", None);
 636        label.push_str(&file_name, comment_id);
 637
 638        let new_text = MentionLink::for_symbol(&symbol.name, &full_path);
 639        let new_text_len = new_text.len();
 640        Some(Completion {
 641            replace_range: source_range.clone(),
 642            new_text,
 643            label,
 644            documentation: None,
 645            source: project::CompletionSource::Custom,
 646            icon_path: Some(IconName::Code.path().into()),
 647            insert_text_mode: None,
 648            confirm: Some(confirm_completion_callback(
 649                IconName::Code.path().into(),
 650                symbol.name.clone().into(),
 651                excerpt_id,
 652                source_range.start,
 653                new_text_len,
 654                editor.clone(),
 655                move |cx| {
 656                    let symbol = symbol.clone();
 657                    let context_store = context_store.clone();
 658                    let workspace = workspace.clone();
 659                    super::symbol_context_picker::add_symbol(
 660                        symbol.clone(),
 661                        false,
 662                        workspace.clone(),
 663                        context_store.downgrade(),
 664                        cx,
 665                    )
 666                    .detach_and_log_err(cx);
 667                },
 668            )),
 669        })
 670    }
 671}
 672
 673fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
 674    let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
 675    let mut label = CodeLabel::default();
 676
 677    label.push_str(&file_name, None);
 678    label.push_str(" ", None);
 679
 680    if let Some(directory) = directory {
 681        label.push_str(&directory, comment_id);
 682    }
 683
 684    label.filter_range = 0..label.text().len();
 685
 686    label
 687}
 688
 689impl CompletionProvider for ContextPickerCompletionProvider {
 690    fn completions(
 691        &self,
 692        excerpt_id: ExcerptId,
 693        buffer: &Entity<Buffer>,
 694        buffer_position: Anchor,
 695        _trigger: CompletionContext,
 696        _window: &mut Window,
 697        cx: &mut Context<Editor>,
 698    ) -> Task<Result<Option<Vec<Completion>>>> {
 699        let state = buffer.update(cx, |buffer, _cx| {
 700            let position = buffer_position.to_point(buffer);
 701            let line_start = Point::new(position.row, 0);
 702            let offset_to_line = buffer.point_to_offset(line_start);
 703            let mut lines = buffer.text_for_range(line_start..position).lines();
 704            let line = lines.next()?;
 705            MentionCompletion::try_parse(line, offset_to_line)
 706        });
 707        let Some(state) = state else {
 708            return Task::ready(Ok(None));
 709        };
 710
 711        let Some((workspace, context_store)) =
 712            self.workspace.upgrade().zip(self.context_store.upgrade())
 713        else {
 714            return Task::ready(Ok(None));
 715        };
 716
 717        let snapshot = buffer.read(cx).snapshot();
 718        let source_range = snapshot.anchor_before(state.source_range.start)
 719            ..snapshot.anchor_before(state.source_range.end);
 720
 721        let thread_store = self.thread_store.clone();
 722        let editor = self.editor.clone();
 723        let http_client = workspace.read(cx).client().http_client().clone();
 724
 725        let MentionCompletion { mode, argument, .. } = state;
 726        let query = argument.unwrap_or_else(|| "".to_string());
 727
 728        let recent_entries = recent_context_picker_entries(
 729            context_store.clone(),
 730            thread_store.clone(),
 731            workspace.clone(),
 732            cx,
 733        );
 734
 735        let search_task = search(
 736            mode,
 737            query,
 738            Arc::<AtomicBool>::default(),
 739            recent_entries,
 740            thread_store.clone(),
 741            workspace.clone(),
 742            cx,
 743        );
 744
 745        cx.spawn(async move |_, cx| {
 746            let matches = search_task.await;
 747            let Some(editor) = editor.upgrade() else {
 748                return Ok(None);
 749            };
 750
 751            Ok(Some(cx.update(|cx| {
 752                matches
 753                    .into_iter()
 754                    .filter_map(|mat| match mat {
 755                        Match::File(FileMatch { mat, is_recent }) => {
 756                            Some(Self::completion_for_path(
 757                                ProjectPath {
 758                                    worktree_id: WorktreeId::from_usize(mat.worktree_id),
 759                                    path: mat.path.clone(),
 760                                },
 761                                &mat.path_prefix,
 762                                is_recent,
 763                                mat.is_dir,
 764                                excerpt_id,
 765                                source_range.clone(),
 766                                editor.clone(),
 767                                context_store.clone(),
 768                                cx,
 769                            ))
 770                        }
 771                        Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
 772                            symbol,
 773                            excerpt_id,
 774                            source_range.clone(),
 775                            editor.clone(),
 776                            context_store.clone(),
 777                            workspace.clone(),
 778                            cx,
 779                        ),
 780                        Match::Thread(ThreadMatch {
 781                            thread, is_recent, ..
 782                        }) => {
 783                            let thread_store = thread_store.as_ref().and_then(|t| t.upgrade())?;
 784                            Some(Self::completion_for_thread(
 785                                thread,
 786                                excerpt_id,
 787                                source_range.clone(),
 788                                is_recent,
 789                                editor.clone(),
 790                                context_store.clone(),
 791                                thread_store,
 792                            ))
 793                        }
 794                        Match::Rules(user_rules) => {
 795                            let thread_store = thread_store.as_ref().and_then(|t| t.upgrade())?;
 796                            Some(Self::completion_for_rules(
 797                                user_rules,
 798                                excerpt_id,
 799                                source_range.clone(),
 800                                editor.clone(),
 801                                context_store.clone(),
 802                                thread_store,
 803                            ))
 804                        }
 805                        Match::Fetch(url) => Some(Self::completion_for_fetch(
 806                            source_range.clone(),
 807                            url,
 808                            excerpt_id,
 809                            editor.clone(),
 810                            context_store.clone(),
 811                            http_client.clone(),
 812                        )),
 813                        Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
 814                            entry,
 815                            excerpt_id,
 816                            source_range.clone(),
 817                            editor.clone(),
 818                            context_store.clone(),
 819                            &workspace,
 820                            cx,
 821                        ),
 822                    })
 823                    .collect()
 824            })?))
 825        })
 826    }
 827
 828    fn resolve_completions(
 829        &self,
 830        _buffer: Entity<Buffer>,
 831        _completion_indices: Vec<usize>,
 832        _completions: Rc<RefCell<Box<[Completion]>>>,
 833        _cx: &mut Context<Editor>,
 834    ) -> Task<Result<bool>> {
 835        Task::ready(Ok(true))
 836    }
 837
 838    fn is_completion_trigger(
 839        &self,
 840        buffer: &Entity<language::Buffer>,
 841        position: language::Anchor,
 842        _: &str,
 843        _: bool,
 844        cx: &mut Context<Editor>,
 845    ) -> bool {
 846        let buffer = buffer.read(cx);
 847        let position = position.to_point(buffer);
 848        let line_start = Point::new(position.row, 0);
 849        let offset_to_line = buffer.point_to_offset(line_start);
 850        let mut lines = buffer.text_for_range(line_start..position).lines();
 851        if let Some(line) = lines.next() {
 852            MentionCompletion::try_parse(line, offset_to_line)
 853                .map(|completion| {
 854                    completion.source_range.start <= offset_to_line + position.column as usize
 855                        && completion.source_range.end >= offset_to_line + position.column as usize
 856                })
 857                .unwrap_or(false)
 858        } else {
 859            false
 860        }
 861    }
 862
 863    fn sort_completions(&self) -> bool {
 864        false
 865    }
 866
 867    fn filter_completions(&self) -> bool {
 868        false
 869    }
 870}
 871
 872fn confirm_completion_callback(
 873    crease_icon_path: SharedString,
 874    crease_text: SharedString,
 875    excerpt_id: ExcerptId,
 876    start: Anchor,
 877    content_len: usize,
 878    editor: Entity<Editor>,
 879    add_context_fn: impl Fn(&mut App) -> () + Send + Sync + 'static,
 880) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
 881    Arc::new(move |_, _, cx| {
 882        add_context_fn(cx);
 883
 884        let crease_text = crease_text.clone();
 885        let crease_icon_path = crease_icon_path.clone();
 886        let editor = editor.clone();
 887        cx.defer(move |cx| {
 888            crate::context_picker::insert_fold_for_mention(
 889                excerpt_id,
 890                start,
 891                content_len,
 892                crease_text,
 893                crease_icon_path,
 894                editor,
 895                cx,
 896            );
 897        });
 898        false
 899    })
 900}
 901
 902#[derive(Debug, Default, PartialEq)]
 903struct MentionCompletion {
 904    source_range: Range<usize>,
 905    mode: Option<ContextPickerMode>,
 906    argument: Option<String>,
 907}
 908
 909impl MentionCompletion {
 910    fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
 911        let last_mention_start = line.rfind('@')?;
 912        if last_mention_start >= line.len() {
 913            return Some(Self::default());
 914        }
 915        if last_mention_start > 0
 916            && line
 917                .chars()
 918                .nth(last_mention_start - 1)
 919                .map_or(false, |c| !c.is_whitespace())
 920        {
 921            return None;
 922        }
 923
 924        let rest_of_line = &line[last_mention_start + 1..];
 925
 926        let mut mode = None;
 927        let mut argument = None;
 928
 929        let mut parts = rest_of_line.split_whitespace();
 930        let mut end = last_mention_start + 1;
 931        if let Some(mode_text) = parts.next() {
 932            end += mode_text.len();
 933
 934            if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok() {
 935                mode = Some(parsed_mode);
 936            } else {
 937                argument = Some(mode_text.to_string());
 938            }
 939            match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
 940                Some(whitespace_count) => {
 941                    if let Some(argument_text) = parts.next() {
 942                        argument = Some(argument_text.to_string());
 943                        end += whitespace_count + argument_text.len();
 944                    }
 945                }
 946                None => {
 947                    // Rest of line is entirely whitespace
 948                    end += rest_of_line.len() - mode_text.len();
 949                }
 950            }
 951        }
 952
 953        Some(Self {
 954            source_range: last_mention_start + offset_to_line..end + offset_to_line,
 955            mode,
 956            argument,
 957        })
 958    }
 959}
 960
 961#[cfg(test)]
 962mod tests {
 963    use super::*;
 964    use editor::AnchorRangeExt;
 965    use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext};
 966    use project::{Project, ProjectPath};
 967    use serde_json::json;
 968    use settings::SettingsStore;
 969    use std::ops::Deref;
 970    use util::{path, separator};
 971    use workspace::{AppState, Item};
 972
 973    #[test]
 974    fn test_mention_completion_parse() {
 975        assert_eq!(MentionCompletion::try_parse("Lorem Ipsum", 0), None);
 976
 977        assert_eq!(
 978            MentionCompletion::try_parse("Lorem @", 0),
 979            Some(MentionCompletion {
 980                source_range: 6..7,
 981                mode: None,
 982                argument: None,
 983            })
 984        );
 985
 986        assert_eq!(
 987            MentionCompletion::try_parse("Lorem @file", 0),
 988            Some(MentionCompletion {
 989                source_range: 6..11,
 990                mode: Some(ContextPickerMode::File),
 991                argument: None,
 992            })
 993        );
 994
 995        assert_eq!(
 996            MentionCompletion::try_parse("Lorem @file ", 0),
 997            Some(MentionCompletion {
 998                source_range: 6..12,
 999                mode: Some(ContextPickerMode::File),
1000                argument: None,
1001            })
1002        );
1003
1004        assert_eq!(
1005            MentionCompletion::try_parse("Lorem @file main.rs", 0),
1006            Some(MentionCompletion {
1007                source_range: 6..19,
1008                mode: Some(ContextPickerMode::File),
1009                argument: Some("main.rs".to_string()),
1010            })
1011        );
1012
1013        assert_eq!(
1014            MentionCompletion::try_parse("Lorem @file main.rs ", 0),
1015            Some(MentionCompletion {
1016                source_range: 6..19,
1017                mode: Some(ContextPickerMode::File),
1018                argument: Some("main.rs".to_string()),
1019            })
1020        );
1021
1022        assert_eq!(
1023            MentionCompletion::try_parse("Lorem @file main.rs Ipsum", 0),
1024            Some(MentionCompletion {
1025                source_range: 6..19,
1026                mode: Some(ContextPickerMode::File),
1027                argument: Some("main.rs".to_string()),
1028            })
1029        );
1030
1031        assert_eq!(
1032            MentionCompletion::try_parse("Lorem @main", 0),
1033            Some(MentionCompletion {
1034                source_range: 6..11,
1035                mode: None,
1036                argument: Some("main".to_string()),
1037            })
1038        );
1039
1040        assert_eq!(MentionCompletion::try_parse("test@", 0), None);
1041    }
1042
1043    struct AtMentionEditor(Entity<Editor>);
1044
1045    impl Item for AtMentionEditor {
1046        type Event = ();
1047
1048        fn include_in_nav_history() -> bool {
1049            false
1050        }
1051    }
1052
1053    impl EventEmitter<()> for AtMentionEditor {}
1054
1055    impl Focusable for AtMentionEditor {
1056        fn focus_handle(&self, cx: &App) -> FocusHandle {
1057            self.0.read(cx).focus_handle(cx).clone()
1058        }
1059    }
1060
1061    impl Render for AtMentionEditor {
1062        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1063            self.0.clone().into_any_element()
1064        }
1065    }
1066
1067    #[gpui::test]
1068    async fn test_context_completion_provider(cx: &mut TestAppContext) {
1069        init_test(cx);
1070
1071        let app_state = cx.update(AppState::test);
1072
1073        cx.update(|cx| {
1074            language::init(cx);
1075            editor::init(cx);
1076            workspace::init(app_state.clone(), cx);
1077            Project::init_settings(cx);
1078        });
1079
1080        app_state
1081            .fs
1082            .as_fake()
1083            .insert_tree(
1084                path!("/dir"),
1085                json!({
1086                    "editor": "",
1087                    "a": {
1088                        "one.txt": "",
1089                        "two.txt": "",
1090                        "three.txt": "",
1091                        "four.txt": ""
1092                    },
1093                    "b": {
1094                        "five.txt": "",
1095                        "six.txt": "",
1096                        "seven.txt": "",
1097                    }
1098                }),
1099            )
1100            .await;
1101
1102        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
1103        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1104        let workspace = window.root(cx).unwrap();
1105
1106        let worktree = project.update(cx, |project, cx| {
1107            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
1108            assert_eq!(worktrees.len(), 1);
1109            worktrees.pop().unwrap()
1110        });
1111        let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
1112
1113        let mut cx = VisualTestContext::from_window(*window.deref(), cx);
1114
1115        let paths = vec![
1116            separator!("a/one.txt"),
1117            separator!("a/two.txt"),
1118            separator!("a/three.txt"),
1119            separator!("a/four.txt"),
1120            separator!("b/five.txt"),
1121            separator!("b/six.txt"),
1122            separator!("b/seven.txt"),
1123        ];
1124        for path in paths {
1125            workspace
1126                .update_in(&mut cx, |workspace, window, cx| {
1127                    workspace.open_path(
1128                        ProjectPath {
1129                            worktree_id,
1130                            path: Path::new(path).into(),
1131                        },
1132                        None,
1133                        false,
1134                        window,
1135                        cx,
1136                    )
1137                })
1138                .await
1139                .unwrap();
1140        }
1141
1142        let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
1143            let editor = cx.new(|cx| {
1144                Editor::new(
1145                    editor::EditorMode::full(),
1146                    multi_buffer::MultiBuffer::build_simple("", cx),
1147                    None,
1148                    window,
1149                    cx,
1150                )
1151            });
1152            workspace.active_pane().update(cx, |pane, cx| {
1153                pane.add_item(
1154                    Box::new(cx.new(|_| AtMentionEditor(editor.clone()))),
1155                    true,
1156                    true,
1157                    None,
1158                    window,
1159                    cx,
1160                );
1161            });
1162            editor
1163        });
1164
1165        let context_store = cx.new(|_| ContextStore::new(project.downgrade(), None));
1166
1167        let editor_entity = editor.downgrade();
1168        editor.update_in(&mut cx, |editor, window, cx| {
1169            window.focus(&editor.focus_handle(cx));
1170            editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
1171                workspace.downgrade(),
1172                context_store.downgrade(),
1173                None,
1174                editor_entity,
1175            ))));
1176        });
1177
1178        cx.simulate_input("Lorem ");
1179
1180        editor.update(&mut cx, |editor, cx| {
1181            assert_eq!(editor.text(cx), "Lorem ");
1182            assert!(!editor.has_visible_completions_menu());
1183        });
1184
1185        cx.simulate_input("@");
1186
1187        editor.update(&mut cx, |editor, cx| {
1188            assert_eq!(editor.text(cx), "Lorem @");
1189            assert!(editor.has_visible_completions_menu());
1190            assert_eq!(
1191                current_completion_labels(editor),
1192                &[
1193                    "seven.txt dir/b/",
1194                    "six.txt dir/b/",
1195                    "five.txt dir/b/",
1196                    "four.txt dir/a/",
1197                    "Files & Directories",
1198                    "Symbols",
1199                    "Fetch"
1200                ]
1201            );
1202        });
1203
1204        // Select and confirm "File"
1205        editor.update_in(&mut cx, |editor, window, cx| {
1206            assert!(editor.has_visible_completions_menu());
1207            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1208            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1209            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1210            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1211            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1212        });
1213
1214        cx.run_until_parked();
1215
1216        editor.update(&mut cx, |editor, cx| {
1217            assert_eq!(editor.text(cx), "Lorem @file ");
1218            assert!(editor.has_visible_completions_menu());
1219        });
1220
1221        cx.simulate_input("one");
1222
1223        editor.update(&mut cx, |editor, cx| {
1224            assert_eq!(editor.text(cx), "Lorem @file one");
1225            assert!(editor.has_visible_completions_menu());
1226            assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
1227        });
1228
1229        editor.update_in(&mut cx, |editor, window, cx| {
1230            assert!(editor.has_visible_completions_menu());
1231            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1232        });
1233
1234        editor.update(&mut cx, |editor, cx| {
1235            assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt)",);
1236            assert!(!editor.has_visible_completions_menu());
1237            assert_eq!(
1238                fold_ranges(editor, cx),
1239                vec![Point::new(0, 6)..Point::new(0, 37)]
1240            );
1241        });
1242
1243        cx.simulate_input(" ");
1244
1245        editor.update(&mut cx, |editor, cx| {
1246            assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt) ",);
1247            assert!(!editor.has_visible_completions_menu());
1248            assert_eq!(
1249                fold_ranges(editor, cx),
1250                vec![Point::new(0, 6)..Point::new(0, 37)]
1251            );
1252        });
1253
1254        cx.simulate_input("Ipsum ");
1255
1256        editor.update(&mut cx, |editor, cx| {
1257            assert_eq!(
1258                editor.text(cx),
1259                "Lorem [@one.txt](@file:dir/a/one.txt) Ipsum ",
1260            );
1261            assert!(!editor.has_visible_completions_menu());
1262            assert_eq!(
1263                fold_ranges(editor, cx),
1264                vec![Point::new(0, 6)..Point::new(0, 37)]
1265            );
1266        });
1267
1268        cx.simulate_input("@file ");
1269
1270        editor.update(&mut cx, |editor, cx| {
1271            assert_eq!(
1272                editor.text(cx),
1273                "Lorem [@one.txt](@file:dir/a/one.txt) Ipsum @file ",
1274            );
1275            assert!(editor.has_visible_completions_menu());
1276            assert_eq!(
1277                fold_ranges(editor, cx),
1278                vec![Point::new(0, 6)..Point::new(0, 37)]
1279            );
1280        });
1281
1282        editor.update_in(&mut cx, |editor, window, cx| {
1283            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1284        });
1285
1286        cx.run_until_parked();
1287
1288        editor.update(&mut cx, |editor, cx| {
1289            assert_eq!(
1290                editor.text(cx),
1291                "Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)"
1292            );
1293            assert!(!editor.has_visible_completions_menu());
1294            assert_eq!(
1295                fold_ranges(editor, cx),
1296                vec![
1297                    Point::new(0, 6)..Point::new(0, 37),
1298                    Point::new(0, 44)..Point::new(0, 79)
1299                ]
1300            );
1301        });
1302
1303        cx.simulate_input("\n@");
1304
1305        editor.update(&mut cx, |editor, cx| {
1306            assert_eq!(
1307                editor.text(cx),
1308                "Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)\n@"
1309            );
1310            assert!(editor.has_visible_completions_menu());
1311            assert_eq!(
1312                fold_ranges(editor, cx),
1313                vec![
1314                    Point::new(0, 6)..Point::new(0, 37),
1315                    Point::new(0, 44)..Point::new(0, 79)
1316                ]
1317            );
1318        });
1319
1320        editor.update_in(&mut cx, |editor, window, cx| {
1321            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1322        });
1323
1324        cx.run_until_parked();
1325
1326        editor.update(&mut cx, |editor, cx| {
1327            assert_eq!(
1328                editor.text(cx),
1329                "Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@seven.txt](@file:dir/b/seven.txt)\n[@six.txt](@file:dir/b/six.txt)"
1330            );
1331            assert!(!editor.has_visible_completions_menu());
1332            assert_eq!(
1333                fold_ranges(editor, cx),
1334                vec![
1335                    Point::new(0, 6)..Point::new(0, 37),
1336                    Point::new(0, 44)..Point::new(0, 79),
1337                    Point::new(1, 0)..Point::new(1, 31)
1338                ]
1339            );
1340        });
1341    }
1342
1343    fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
1344        let snapshot = editor.buffer().read(cx).snapshot(cx);
1345        editor.display_map.update(cx, |display_map, cx| {
1346            display_map
1347                .snapshot(cx)
1348                .folds_in_range(0..snapshot.len())
1349                .map(|fold| fold.range.to_point(&snapshot))
1350                .collect()
1351        })
1352    }
1353
1354    fn current_completion_labels(editor: &Editor) -> Vec<String> {
1355        let completions = editor.current_completions().expect("Missing completions");
1356        completions
1357            .into_iter()
1358            .map(|completion| completion.label.text.to_string())
1359            .collect::<Vec<_>>()
1360    }
1361
1362    pub(crate) fn init_test(cx: &mut TestAppContext) {
1363        cx.update(|cx| {
1364            let store = SettingsStore::test(cx);
1365            cx.set_global(store);
1366            theme::init(theme::LoadThemes::JustBase, cx);
1367            client::init_settings(cx);
1368            language::init(cx);
1369            Project::init_settings(cx);
1370            workspace::init_settings(cx);
1371            editor::init_settings(cx);
1372        });
1373    }
1374}