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 "File/Directory",
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}