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