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