completion_provider.rs

  1use std::cell::RefCell;
  2use std::ops::Range;
  3use std::path::Path;
  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                } => 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::MessageCircle
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::MessageCircle.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
313impl CompletionProvider for ContextPickerCompletionProvider {
314    fn completions(
315        &self,
316        excerpt_id: ExcerptId,
317        buffer: &Entity<Buffer>,
318        buffer_position: Anchor,
319        _trigger: CompletionContext,
320        _window: &mut Window,
321        cx: &mut Context<Editor>,
322    ) -> Task<Result<Option<Vec<Completion>>>> {
323        let state = buffer.update(cx, |buffer, _cx| {
324            let position = buffer_position.to_point(buffer);
325            let line_start = Point::new(position.row, 0);
326            let offset_to_line = buffer.point_to_offset(line_start);
327            let mut lines = buffer.text_for_range(line_start..position).lines();
328            let line = lines.next()?;
329            MentionCompletion::try_parse(line, offset_to_line)
330        });
331        let Some(state) = state else {
332            return Task::ready(Ok(None));
333        };
334
335        let Some(workspace) = self.workspace.upgrade() else {
336            return Task::ready(Ok(None));
337        };
338        let Some(context_store) = self.context_store.upgrade() else {
339            return Task::ready(Ok(None));
340        };
341
342        let snapshot = buffer.read(cx).snapshot();
343        let source_range = snapshot.anchor_after(state.source_range.start)
344            ..snapshot.anchor_before(state.source_range.end);
345
346        let thread_store = self.thread_store.clone();
347        let editor = self.editor.clone();
348        let http_client = workspace.read(cx).client().http_client().clone();
349
350        cx.spawn(async move |_, cx| {
351            let mut completions = Vec::new();
352
353            let MentionCompletion {
354                mode: category,
355                argument,
356                ..
357            } = state;
358
359            let query = argument.unwrap_or_else(|| "".to_string());
360            match category {
361                Some(ContextPickerMode::File) => {
362                    let path_matches = cx
363                        .update(|cx| {
364                            super::file_context_picker::search_paths(
365                                query,
366                                Arc::<AtomicBool>::default(),
367                                &workspace,
368                                cx,
369                            )
370                        })?
371                        .await;
372
373                    if let Some(editor) = editor.upgrade() {
374                        completions.reserve(path_matches.len());
375                        cx.update(|cx| {
376                            completions.extend(path_matches.iter().map(|mat| {
377                                Self::completion_for_path(
378                                    ProjectPath {
379                                        worktree_id: WorktreeId::from_usize(mat.worktree_id),
380                                        path: mat.path.clone(),
381                                    },
382                                    &mat.path_prefix,
383                                    false,
384                                    mat.is_dir,
385                                    excerpt_id,
386                                    source_range.clone(),
387                                    editor.clone(),
388                                    context_store.clone(),
389                                    cx,
390                                )
391                            }));
392                        })?;
393                    }
394                }
395                Some(ContextPickerMode::Fetch) => {
396                    if let Some(editor) = editor.upgrade() {
397                        if !query.is_empty() {
398                            completions.push(Self::completion_for_fetch(
399                                source_range.clone(),
400                                query.into(),
401                                excerpt_id,
402                                editor.clone(),
403                                context_store.clone(),
404                                http_client.clone(),
405                            ));
406                        }
407
408                        context_store.update(cx, |store, _| {
409                            let urls = store.context().iter().filter_map(|context| {
410                                if let AssistantContext::FetchedUrl(context) = context {
411                                    Some(context.url.clone())
412                                } else {
413                                    None
414                                }
415                            });
416                            for url in urls {
417                                completions.push(Self::completion_for_fetch(
418                                    source_range.clone(),
419                                    url,
420                                    excerpt_id,
421                                    editor.clone(),
422                                    context_store.clone(),
423                                    http_client.clone(),
424                                ));
425                            }
426                        })?;
427                    }
428                }
429                Some(ContextPickerMode::Thread) => {
430                    if let Some((thread_store, editor)) = thread_store
431                        .and_then(|thread_store| thread_store.upgrade())
432                        .zip(editor.upgrade())
433                    {
434                        let threads = cx
435                            .update(|cx| {
436                                super::thread_context_picker::search_threads(
437                                    query,
438                                    thread_store.clone(),
439                                    cx,
440                                )
441                            })?
442                            .await;
443                        for thread in threads {
444                            completions.push(Self::completion_for_thread(
445                                thread.clone(),
446                                excerpt_id,
447                                source_range.clone(),
448                                false,
449                                editor.clone(),
450                                context_store.clone(),
451                                thread_store.clone(),
452                            ));
453                        }
454                    }
455                }
456                None => {
457                    cx.update(|cx| {
458                        if let Some(editor) = editor.upgrade() {
459                            completions.extend(Self::default_completions(
460                                excerpt_id,
461                                source_range.clone(),
462                                context_store.clone(),
463                                thread_store.clone(),
464                                editor,
465                                workspace.clone(),
466                                cx,
467                            ));
468                        }
469                    })?;
470                }
471            }
472            Ok(Some(completions))
473        })
474    }
475
476    fn resolve_completions(
477        &self,
478        _buffer: Entity<Buffer>,
479        _completion_indices: Vec<usize>,
480        _completions: Rc<RefCell<Box<[Completion]>>>,
481        _cx: &mut Context<Editor>,
482    ) -> Task<Result<bool>> {
483        Task::ready(Ok(true))
484    }
485
486    fn is_completion_trigger(
487        &self,
488        buffer: &Entity<language::Buffer>,
489        position: language::Anchor,
490        _: &str,
491        _: bool,
492        cx: &mut Context<Editor>,
493    ) -> bool {
494        let buffer = buffer.read(cx);
495        let position = position.to_point(buffer);
496        let line_start = Point::new(position.row, 0);
497        let offset_to_line = buffer.point_to_offset(line_start);
498        let mut lines = buffer.text_for_range(line_start..position).lines();
499        if let Some(line) = lines.next() {
500            MentionCompletion::try_parse(line, offset_to_line)
501                .map(|completion| {
502                    completion.source_range.start <= offset_to_line + position.column as usize
503                        && completion.source_range.end >= offset_to_line + position.column as usize
504                })
505                .unwrap_or(false)
506        } else {
507            false
508        }
509    }
510
511    fn sort_completions(&self) -> bool {
512        false
513    }
514
515    fn filter_completions(&self) -> bool {
516        false
517    }
518}
519
520fn confirm_completion_callback(
521    crease_icon_path: SharedString,
522    crease_text: SharedString,
523    excerpt_id: ExcerptId,
524    start: Anchor,
525    content_len: usize,
526    editor: Entity<Editor>,
527    add_context_fn: impl Fn(&mut App) -> () + Send + Sync + 'static,
528) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
529    Arc::new(move |_, window, cx| {
530        add_context_fn(cx);
531
532        let crease_text = crease_text.clone();
533        let crease_icon_path = crease_icon_path.clone();
534        let editor = editor.clone();
535        window.defer(cx, move |window, cx| {
536            crate::context_picker::insert_crease_for_mention(
537                excerpt_id,
538                start,
539                content_len,
540                crease_text,
541                crease_icon_path,
542                editor,
543                window,
544                cx,
545            );
546        });
547        false
548    })
549}
550
551#[derive(Debug, Default, PartialEq)]
552struct MentionCompletion {
553    source_range: Range<usize>,
554    mode: Option<ContextPickerMode>,
555    argument: Option<String>,
556}
557
558impl MentionCompletion {
559    fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
560        let last_mention_start = line.rfind('@')?;
561        if last_mention_start >= line.len() {
562            return Some(Self::default());
563        }
564        let rest_of_line = &line[last_mention_start + 1..];
565
566        let mut mode = None;
567        let mut argument = None;
568
569        let mut parts = rest_of_line.split_whitespace();
570        let mut end = last_mention_start + 1;
571        if let Some(mode_text) = parts.next() {
572            end += mode_text.len();
573            mode = ContextPickerMode::try_from(mode_text).ok();
574            match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
575                Some(whitespace_count) => {
576                    if let Some(argument_text) = parts.next() {
577                        argument = Some(argument_text.to_string());
578                        end += whitespace_count + argument_text.len();
579                    }
580                }
581                None => {
582                    // Rest of line is entirely whitespace
583                    end += rest_of_line.len() - mode_text.len();
584                }
585            }
586        }
587
588        Some(Self {
589            source_range: last_mention_start + offset_to_line..end + offset_to_line,
590            mode,
591            argument,
592        })
593    }
594}
595
596#[cfg(test)]
597mod tests {
598    use super::*;
599    use gpui::{Focusable, TestAppContext, VisualTestContext};
600    use project::{Project, ProjectPath};
601    use serde_json::json;
602    use settings::SettingsStore;
603    use std::{ops::Deref, path::PathBuf};
604    use util::{path, separator};
605    use workspace::AppState;
606
607    #[test]
608    fn test_mention_completion_parse() {
609        assert_eq!(MentionCompletion::try_parse("Lorem Ipsum", 0), None);
610
611        assert_eq!(
612            MentionCompletion::try_parse("Lorem @", 0),
613            Some(MentionCompletion {
614                source_range: 6..7,
615                mode: None,
616                argument: None,
617            })
618        );
619
620        assert_eq!(
621            MentionCompletion::try_parse("Lorem @file", 0),
622            Some(MentionCompletion {
623                source_range: 6..11,
624                mode: Some(ContextPickerMode::File),
625                argument: None,
626            })
627        );
628
629        assert_eq!(
630            MentionCompletion::try_parse("Lorem @file ", 0),
631            Some(MentionCompletion {
632                source_range: 6..12,
633                mode: Some(ContextPickerMode::File),
634                argument: None,
635            })
636        );
637
638        assert_eq!(
639            MentionCompletion::try_parse("Lorem @file main.rs", 0),
640            Some(MentionCompletion {
641                source_range: 6..19,
642                mode: Some(ContextPickerMode::File),
643                argument: Some("main.rs".to_string()),
644            })
645        );
646
647        assert_eq!(
648            MentionCompletion::try_parse("Lorem @file main.rs ", 0),
649            Some(MentionCompletion {
650                source_range: 6..19,
651                mode: Some(ContextPickerMode::File),
652                argument: Some("main.rs".to_string()),
653            })
654        );
655
656        assert_eq!(
657            MentionCompletion::try_parse("Lorem @file main.rs Ipsum", 0),
658            Some(MentionCompletion {
659                source_range: 6..19,
660                mode: Some(ContextPickerMode::File),
661                argument: Some("main.rs".to_string()),
662            })
663        );
664    }
665
666    #[gpui::test]
667    async fn test_context_completion_provider(cx: &mut TestAppContext) {
668        init_test(cx);
669
670        let app_state = cx.update(AppState::test);
671
672        cx.update(|cx| {
673            language::init(cx);
674            editor::init(cx);
675            workspace::init(app_state.clone(), cx);
676            Project::init_settings(cx);
677        });
678
679        app_state
680            .fs
681            .as_fake()
682            .insert_tree(
683                path!("/dir"),
684                json!({
685                    "editor": "",
686                    "a": {
687                        "one.txt": "",
688                        "two.txt": "",
689                        "three.txt": "",
690                        "four.txt": ""
691                    },
692                    "b": {
693                        "five.txt": "",
694                        "six.txt": "",
695                        "seven.txt": "",
696                    }
697                }),
698            )
699            .await;
700
701        let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
702        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
703        let workspace = window.root(cx).unwrap();
704
705        let worktree = project.update(cx, |project, cx| {
706            let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
707            assert_eq!(worktrees.len(), 1);
708            worktrees.pop().unwrap()
709        });
710        let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
711
712        let mut cx = VisualTestContext::from_window(*window.deref(), cx);
713
714        let paths = vec![
715            separator!("a/one.txt"),
716            separator!("a/two.txt"),
717            separator!("a/three.txt"),
718            separator!("a/four.txt"),
719            separator!("b/five.txt"),
720            separator!("b/six.txt"),
721            separator!("b/seven.txt"),
722        ];
723        for path in paths {
724            workspace
725                .update_in(&mut cx, |workspace, window, cx| {
726                    workspace.open_path(
727                        ProjectPath {
728                            worktree_id,
729                            path: Path::new(path).into(),
730                        },
731                        None,
732                        false,
733                        window,
734                        cx,
735                    )
736                })
737                .await
738                .unwrap();
739        }
740
741        let item = workspace
742            .update_in(&mut cx, |workspace, window, cx| {
743                workspace.open_path(
744                    ProjectPath {
745                        worktree_id,
746                        path: PathBuf::from("editor").into(),
747                    },
748                    None,
749                    true,
750                    window,
751                    cx,
752                )
753            })
754            .await
755            .expect("Could not open test file");
756
757        let editor = cx.update(|_, cx| {
758            item.act_as::<Editor>(cx)
759                .expect("Opened test file wasn't an editor")
760        });
761
762        let context_store = cx.new(|_| ContextStore::new(workspace.downgrade()));
763
764        let editor_entity = editor.downgrade();
765        editor.update_in(&mut cx, |editor, window, cx| {
766            window.focus(&editor.focus_handle(cx));
767            editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
768                workspace.downgrade(),
769                context_store.downgrade(),
770                None,
771                editor_entity,
772            ))));
773        });
774
775        cx.simulate_input("Lorem ");
776
777        editor.update(&mut cx, |editor, cx| {
778            assert_eq!(editor.text(cx), "Lorem ");
779            assert!(!editor.has_visible_completions_menu());
780        });
781
782        cx.simulate_input("@");
783
784        editor.update(&mut cx, |editor, cx| {
785            assert_eq!(editor.text(cx), "Lorem @");
786            assert!(editor.has_visible_completions_menu());
787            assert_eq!(
788                current_completion_labels(editor),
789                &[
790                    "seven.txt dir/b/",
791                    "six.txt dir/b/",
792                    "five.txt dir/b/",
793                    "four.txt dir/a/",
794                    "Files & Directories",
795                    "Fetch"
796                ]
797            );
798        });
799
800        // Select and confirm "File"
801        editor.update_in(&mut cx, |editor, window, cx| {
802            assert!(editor.has_visible_completions_menu());
803            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
804            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
805            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
806            editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
807            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
808        });
809
810        cx.run_until_parked();
811
812        editor.update(&mut cx, |editor, cx| {
813            assert_eq!(editor.text(cx), "Lorem @file ");
814            assert!(editor.has_visible_completions_menu());
815        });
816
817        cx.simulate_input("one");
818
819        editor.update(&mut cx, |editor, cx| {
820            assert_eq!(editor.text(cx), "Lorem @file one");
821            assert!(editor.has_visible_completions_menu());
822            assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
823        });
824
825        editor.update_in(&mut cx, |editor, window, cx| {
826            assert!(editor.has_visible_completions_menu());
827            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
828        });
829
830        editor.update(&mut cx, |editor, cx| {
831            assert_eq!(editor.text(cx), "Lorem @file dir/a/one.txt",);
832            assert!(!editor.has_visible_completions_menu());
833            assert_eq!(
834                crease_ranges(editor, cx),
835                vec![Point::new(0, 6)..Point::new(0, 25)]
836            );
837        });
838
839        cx.simulate_input(" ");
840
841        editor.update(&mut cx, |editor, cx| {
842            assert_eq!(editor.text(cx), "Lorem @file dir/a/one.txt ",);
843            assert!(!editor.has_visible_completions_menu());
844            assert_eq!(
845                crease_ranges(editor, cx),
846                vec![Point::new(0, 6)..Point::new(0, 25)]
847            );
848        });
849
850        cx.simulate_input("Ipsum ");
851
852        editor.update(&mut cx, |editor, cx| {
853            assert_eq!(editor.text(cx), "Lorem @file dir/a/one.txt Ipsum ",);
854            assert!(!editor.has_visible_completions_menu());
855            assert_eq!(
856                crease_ranges(editor, cx),
857                vec![Point::new(0, 6)..Point::new(0, 25)]
858            );
859        });
860
861        cx.simulate_input("@file ");
862
863        editor.update(&mut cx, |editor, cx| {
864            assert_eq!(editor.text(cx), "Lorem @file dir/a/one.txt Ipsum @file ",);
865            assert!(editor.has_visible_completions_menu());
866            assert_eq!(
867                crease_ranges(editor, cx),
868                vec![Point::new(0, 6)..Point::new(0, 25)]
869            );
870        });
871
872        editor.update_in(&mut cx, |editor, window, cx| {
873            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
874        });
875
876        cx.run_until_parked();
877
878        editor.update(&mut cx, |editor, cx| {
879            assert_eq!(
880                editor.text(cx),
881                "Lorem @file dir/a/one.txt Ipsum @file dir/b/seven.txt"
882            );
883            assert!(!editor.has_visible_completions_menu());
884            assert_eq!(
885                crease_ranges(editor, cx),
886                vec![
887                    Point::new(0, 6)..Point::new(0, 25),
888                    Point::new(0, 32)..Point::new(0, 53)
889                ]
890            );
891        });
892
893        cx.simulate_input("\n@");
894
895        editor.update(&mut cx, |editor, cx| {
896            assert_eq!(
897                editor.text(cx),
898                "Lorem @file dir/a/one.txt Ipsum @file dir/b/seven.txt\n@"
899            );
900            assert!(editor.has_visible_completions_menu());
901            assert_eq!(
902                crease_ranges(editor, cx),
903                vec![
904                    Point::new(0, 6)..Point::new(0, 25),
905                    Point::new(0, 32)..Point::new(0, 53)
906                ]
907            );
908        });
909
910        editor.update_in(&mut cx, |editor, window, cx| {
911            editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
912        });
913
914        cx.run_until_parked();
915
916        editor.update(&mut cx, |editor, cx| {
917            assert_eq!(
918                editor.text(cx),
919                "Lorem @file dir/a/one.txt Ipsum @file dir/b/seven.txt\n@file dir/b/six.txt"
920            );
921            assert!(!editor.has_visible_completions_menu());
922            assert_eq!(
923                crease_ranges(editor, cx),
924                vec![
925                    Point::new(0, 6)..Point::new(0, 25),
926                    Point::new(0, 32)..Point::new(0, 53),
927                    Point::new(1, 0)..Point::new(1, 19)
928                ]
929            );
930        });
931    }
932
933    fn crease_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
934        let snapshot = editor.buffer().read(cx).snapshot(cx);
935        editor.display_map.update(cx, |display_map, cx| {
936            display_map
937                .snapshot(cx)
938                .crease_snapshot
939                .crease_items_with_offsets(&snapshot)
940                .into_iter()
941                .map(|(_, range)| range)
942                .collect()
943        })
944    }
945
946    fn current_completion_labels(editor: &Editor) -> Vec<String> {
947        let completions = editor.current_completions().expect("Missing completions");
948        completions
949            .into_iter()
950            .map(|completion| completion.label.text.to_string())
951            .collect::<Vec<_>>()
952    }
953
954    pub(crate) fn init_test(cx: &mut TestAppContext) {
955        cx.update(|cx| {
956            let store = SettingsStore::test(cx);
957            cx.set_global(store);
958            theme::init(theme::LoadThemes::JustBase, cx);
959            client::init_settings(cx);
960            language::init(cx);
961            Project::init_settings(cx);
962            workspace::init_settings(cx);
963            editor::init_settings(cx);
964        });
965    }
966}