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