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