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