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