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