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}