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
 549fn confirm_completion_callback(
 550    crease_icon_path: SharedString,
 551    crease_text: SharedString,
 552    excerpt_id: ExcerptId,
 553    start: Anchor,
 554    content_len: usize,
 555    editor: Entity<Editor>,
 556    add_context_fn: impl Fn(&mut App) -> () + Send + Sync + 'static,
 557) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
 558    Arc::new(move |_, window, cx| {
 559        add_context_fn(cx);
 560
 561        let crease_text = crease_text.clone();
 562        let crease_icon_path = crease_icon_path.clone();
 563        let editor = editor.clone();
 564        window.defer(cx, move |window, cx| {
 565            crate::context_picker::insert_crease_for_mention(
 566                excerpt_id,
 567                start,
 568                content_len,
 569                crease_text,
 570                crease_icon_path,
 571                editor,
 572                window,
 573                cx,
 574            );
 575        });
 576        false
 577    })
 578}
 579
 580#[derive(Debug, Default, PartialEq)]
 581struct MentionCompletion {
 582    source_range: Range<usize>,
 583    mode: Option<ContextPickerMode>,
 584    argument: Option<String>,
 585}
 586
 587impl MentionCompletion {
 588    fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
 589        let last_mention_start = line.rfind('@')?;
 590        if last_mention_start >= line.len() {
 591            return Some(Self::default());
 592        }
 593        let rest_of_line = &line[last_mention_start + 1..];
 594
 595        let mut mode = None;
 596        let mut argument = None;
 597
 598        let mut parts = rest_of_line.split_whitespace();
 599        let mut end = last_mention_start + 1;
 600        if let Some(mode_text) = parts.next() {
 601            end += mode_text.len();
 602            mode = ContextPickerMode::try_from(mode_text).ok();
 603            match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
 604                Some(whitespace_count) => {
 605                    if let Some(argument_text) = parts.next() {
 606                        argument = Some(argument_text.to_string());
 607                        end += whitespace_count + argument_text.len();
 608                    }
 609                }
 610                None => {
 611                    // Rest of line is entirely whitespace
 612                    end += rest_of_line.len() - mode_text.len();
 613                }
 614            }
 615        }
 616
 617        Some(Self {
 618            source_range: last_mention_start + offset_to_line..end + offset_to_line,
 619            mode,
 620            argument,
 621        })
 622    }
 623}
 624
 625#[cfg(test)]
 626mod tests {
 627    use super::*;
 628    use gpui::{Focusable, TestAppContext, VisualTestContext};
 629    use project::{Project, ProjectPath};
 630    use serde_json::json;
 631    use settings::SettingsStore;
 632    use std::{ops::Deref, path::PathBuf};
 633    use util::{path, separator};
 634    use workspace::AppState;
 635
 636    #[test]
 637    fn test_mention_completion_parse() {
 638        assert_eq!(MentionCompletion::try_parse("Lorem Ipsum", 0), None);
 639
 640        assert_eq!(
 641            MentionCompletion::try_parse("Lorem @", 0),
 642            Some(MentionCompletion {
 643                source_range: 6..7,
 644                mode: None,
 645                argument: None,
 646            })
 647        );
 648
 649        assert_eq!(
 650            MentionCompletion::try_parse("Lorem @file", 0),
 651            Some(MentionCompletion {
 652                source_range: 6..11,
 653                mode: Some(ContextPickerMode::File),
 654                argument: None,
 655            })
 656        );
 657
 658        assert_eq!(
 659            MentionCompletion::try_parse("Lorem @file ", 0),
 660            Some(MentionCompletion {
 661                source_range: 6..12,
 662                mode: Some(ContextPickerMode::File),
 663                argument: None,
 664            })
 665        );
 666
 667        assert_eq!(
 668            MentionCompletion::try_parse("Lorem @file main.rs", 0),
 669            Some(MentionCompletion {
 670                source_range: 6..19,
 671                mode: Some(ContextPickerMode::File),
 672                argument: Some("main.rs".to_string()),
 673            })
 674        );
 675
 676        assert_eq!(
 677            MentionCompletion::try_parse("Lorem @file main.rs ", 0),
 678            Some(MentionCompletion {
 679                source_range: 6..19,
 680                mode: Some(ContextPickerMode::File),
 681                argument: Some("main.rs".to_string()),
 682            })
 683        );
 684
 685        assert_eq!(
 686            MentionCompletion::try_parse("Lorem @file main.rs Ipsum", 0),
 687            Some(MentionCompletion {
 688                source_range: 6..19,
 689                mode: Some(ContextPickerMode::File),
 690                argument: Some("main.rs".to_string()),
 691            })
 692        );
 693    }
 694
 695    #[gpui::test]
 696    async fn test_context_completion_provider(cx: &mut TestAppContext) {
 697        init_test(cx);
 698
 699        let app_state = cx.update(AppState::test);
 700
 701        cx.update(|cx| {
 702            language::init(cx);
 703            editor::init(cx);
 704            workspace::init(app_state.clone(), cx);
 705            Project::init_settings(cx);
 706        });
 707
 708        app_state
 709            .fs
 710            .as_fake()
 711            .insert_tree(
 712                path!("/dir"),
 713                json!({
 714                    "editor": "",
 715                    "a": {
 716                        "one.txt": "",
 717                        "two.txt": "",
 718                        "three.txt": "",
 719                        "four.txt": ""
 720                    },
 721                    "b": {
 722                        "five.txt": "",
 723                        "six.txt": "",
 724                        "seven.txt": "",
 725                    }
 726                }),
 727            )
 728            .await;
 729
 730        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
 731        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
 732        let workspace = window.root(cx).unwrap();
 733
 734        let worktree = project.update(cx, |project, cx| {
 735            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
 736            assert_eq!(worktrees.len(), 1);
 737            worktrees.pop().unwrap()
 738        });
 739        let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
 740
 741        let mut cx = VisualTestContext::from_window(*window.deref(), cx);
 742
 743        let paths = vec![
 744            separator!("a/one.txt"),
 745            separator!("a/two.txt"),
 746            separator!("a/three.txt"),
 747            separator!("a/four.txt"),
 748            separator!("b/five.txt"),
 749            separator!("b/six.txt"),
 750            separator!("b/seven.txt"),
 751        ];
 752        for path in paths {
 753            workspace
 754                .update_in(&mut cx, |workspace, window, cx| {
 755                    workspace.open_path(
 756                        ProjectPath {
 757                            worktree_id,
 758                            path: Path::new(path).into(),
 759                        },
 760                        None,
 761                        false,
 762                        window,
 763                        cx,
 764                    )
 765                })
 766                .await
 767                .unwrap();
 768        }
 769
 770        //TODO: Construct the editor without an actual buffer that points to a file
 771        let item = workspace
 772            .update_in(&mut cx, |workspace, window, cx| {
 773                workspace.open_path(
 774                    ProjectPath {
 775                        worktree_id,
 776                        path: PathBuf::from("editor").into(),
 777                    },
 778                    None,
 779                    true,
 780                    window,
 781                    cx,
 782                )
 783            })
 784            .await
 785            .expect("Could not open test file");
 786
 787        let editor = cx.update(|_, cx| {
 788            item.act_as::<Editor>(cx)
 789                .expect("Opened test file wasn't an editor")
 790        });
 791
 792        let context_store = cx.new(|_| ContextStore::new(workspace.downgrade()));
 793
 794        let editor_entity = editor.downgrade();
 795        editor.update_in(&mut cx, |editor, window, cx| {
 796            window.focus(&editor.focus_handle(cx));
 797            editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
 798                workspace.downgrade(),
 799                context_store.downgrade(),
 800                None,
 801                editor_entity,
 802            ))));
 803        });
 804
 805        cx.simulate_input("Lorem ");
 806
 807        editor.update(&mut cx, |editor, cx| {
 808            assert_eq!(editor.text(cx), "Lorem ");
 809            assert!(!editor.has_visible_completions_menu());
 810        });
 811
 812        cx.simulate_input("@");
 813
 814        editor.update(&mut cx, |editor, cx| {
 815            assert_eq!(editor.text(cx), "Lorem @");
 816            assert!(editor.has_visible_completions_menu());
 817            assert_eq!(
 818                current_completion_labels(editor),
 819                &[
 820                    format!("seven.txt {}", separator!("dir/b")).as_str(),
 821                    format!("six.txt {}", separator!("dir/b")).as_str(),
 822                    format!("five.txt {}", separator!("dir/b")).as_str(),
 823                    format!("four.txt {}", separator!("dir/a")).as_str(),
 824                    "Files & Directories",
 825                    "Fetch"
 826                ]
 827            );
 828        });
 829
 830        // Select and confirm "File"
 831        editor.update_in(&mut cx, |editor, window, cx| {
 832            assert!(editor.has_visible_completions_menu());
 833            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
 834            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
 835            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
 836            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
 837            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
 838        });
 839
 840        cx.run_until_parked();
 841
 842        editor.update(&mut cx, |editor, cx| {
 843            assert_eq!(editor.text(cx), "Lorem @file ");
 844            assert!(editor.has_visible_completions_menu());
 845        });
 846
 847        cx.simulate_input("one");
 848
 849        editor.update(&mut cx, |editor, cx| {
 850            assert_eq!(editor.text(cx), "Lorem @file one");
 851            assert!(editor.has_visible_completions_menu());
 852            assert_eq!(
 853                current_completion_labels(editor),
 854                vec![format!("one.txt {}", separator!("dir/a")).as_str(),]
 855            );
 856        });
 857
 858        editor.update_in(&mut cx, |editor, window, cx| {
 859            assert!(editor.has_visible_completions_menu());
 860            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
 861        });
 862
 863        editor.update(&mut cx, |editor, cx| {
 864            assert_eq!(
 865                editor.text(cx),
 866                format!("Lorem @file {}", separator!("dir/a/one.txt"))
 867            );
 868            assert!(!editor.has_visible_completions_menu());
 869            assert_eq!(
 870                crease_ranges(editor, cx),
 871                vec![Point::new(0, 6)..Point::new(0, 25)]
 872            );
 873        });
 874
 875        cx.simulate_input(" ");
 876
 877        editor.update(&mut cx, |editor, cx| {
 878            assert_eq!(
 879                editor.text(cx),
 880                format!("Lorem @file {} ", separator!("dir/a/one.txt"))
 881            );
 882            assert!(!editor.has_visible_completions_menu());
 883            assert_eq!(
 884                crease_ranges(editor, cx),
 885                vec![Point::new(0, 6)..Point::new(0, 25)]
 886            );
 887        });
 888
 889        cx.simulate_input("Ipsum ");
 890
 891        editor.update(&mut cx, |editor, cx| {
 892            assert_eq!(
 893                editor.text(cx),
 894                format!("Lorem @file {} Ipsum ", separator!("dir/a/one.txt"))
 895            );
 896            assert!(!editor.has_visible_completions_menu());
 897            assert_eq!(
 898                crease_ranges(editor, cx),
 899                vec![Point::new(0, 6)..Point::new(0, 25)]
 900            );
 901        });
 902
 903        cx.simulate_input("@file ");
 904
 905        editor.update(&mut cx, |editor, cx| {
 906            assert_eq!(
 907                editor.text(cx),
 908                format!("Lorem @file {} Ipsum @file ", separator!("dir/a/one.txt"))
 909            );
 910            assert!(editor.has_visible_completions_menu());
 911            assert_eq!(
 912                crease_ranges(editor, cx),
 913                vec![Point::new(0, 6)..Point::new(0, 25)]
 914            );
 915        });
 916
 917        editor.update_in(&mut cx, |editor, window, cx| {
 918            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
 919        });
 920
 921        cx.run_until_parked();
 922
 923        editor.update(&mut cx, |editor, cx| {
 924            assert_eq!(
 925                editor.text(cx),
 926                format!(
 927                    "Lorem @file {} Ipsum @file {}",
 928                    separator!("dir/a/one.txt"),
 929                    separator!("dir/b/seven.txt")
 930                )
 931            );
 932            assert!(!editor.has_visible_completions_menu());
 933            assert_eq!(
 934                crease_ranges(editor, cx),
 935                vec![
 936                    Point::new(0, 6)..Point::new(0, 25),
 937                    Point::new(0, 32)..Point::new(0, 53)
 938                ]
 939            );
 940        });
 941
 942        cx.simulate_input("\n@");
 943
 944        editor.update(&mut cx, |editor, cx| {
 945            assert_eq!(
 946                editor.text(cx),
 947                format!(
 948                    "Lorem @file {} Ipsum @file {}\n@",
 949                    separator!("dir/a/one.txt"),
 950                    separator!("dir/b/seven.txt")
 951                )
 952            );
 953            assert!(editor.has_visible_completions_menu());
 954            assert_eq!(
 955                crease_ranges(editor, cx),
 956                vec![
 957                    Point::new(0, 6)..Point::new(0, 25),
 958                    Point::new(0, 32)..Point::new(0, 53)
 959                ]
 960            );
 961        });
 962
 963        editor.update_in(&mut cx, |editor, window, cx| {
 964            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
 965        });
 966
 967        cx.run_until_parked();
 968
 969        editor.update(&mut cx, |editor, cx| {
 970            assert_eq!(
 971                editor.text(cx),
 972                format!(
 973                    "Lorem @file {} Ipsum @file {}\n@file {}",
 974                    separator!("dir/a/one.txt"),
 975                    separator!("dir/b/seven.txt"),
 976                    separator!("dir/b/six.txt"),
 977                )
 978            );
 979            assert!(!editor.has_visible_completions_menu());
 980            assert_eq!(
 981                crease_ranges(editor, cx),
 982                vec![
 983                    Point::new(0, 6)..Point::new(0, 25),
 984                    Point::new(0, 32)..Point::new(0, 53),
 985                    Point::new(1, 0)..Point::new(1, 19)
 986                ]
 987            );
 988        });
 989    }
 990
 991    fn crease_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
 992        let snapshot = editor.buffer().read(cx).snapshot(cx);
 993        editor.display_map.update(cx, |display_map, cx| {
 994            display_map
 995                .snapshot(cx)
 996                .crease_snapshot
 997                .crease_items_with_offsets(&snapshot)
 998                .into_iter()
 999                .map(|(_, range)| range)
1000                .collect()
1001        })
1002    }
1003
1004    fn current_completion_labels(editor: &Editor) -> Vec<String> {
1005        let completions = editor.current_completions().expect("Missing completions");
1006        completions
1007            .into_iter()
1008            .map(|completion| completion.label.text.to_string())
1009            .collect::<Vec<_>>()
1010    }
1011
1012    pub(crate) fn init_test(cx: &mut TestAppContext) {
1013        cx.update(|cx| {
1014            let store = SettingsStore::test(cx);
1015            cx.set_global(store);
1016            theme::init(theme::LoadThemes::JustBase, cx);
1017            client::init_settings(cx);
1018            language::init(cx);
1019            Project::init_settings(cx);
1020            workspace::init_settings(cx);
1021            editor::init_settings(cx);
1022        });
1023    }
1024}