completion_provider.rs

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