completion_provider.rs

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