1use std::ops::Range;
2use std::path::PathBuf;
3use std::sync::Arc;
4use std::sync::atomic::AtomicBool;
5
6use acp_thread::MentionUri;
7use anyhow::{Context as _, Result, anyhow};
8use collections::{HashMap, HashSet};
9use editor::display_map::CreaseId;
10use editor::{CompletionProvider, Editor, ExcerptId};
11use futures::future::{Shared, try_join_all};
12use fuzzy::{StringMatch, StringMatchCandidate};
13use gpui::{App, Entity, ImageFormat, Task, WeakEntity};
14use language::{Buffer, CodeLabel, HighlightId};
15use lsp::CompletionContext;
16use project::{
17 Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId,
18};
19use prompt_store::PromptStore;
20use rope::Point;
21use text::{Anchor, ToPoint as _};
22use ui::prelude::*;
23use url::Url;
24use workspace::Workspace;
25
26use agent::thread_store::{TextThreadStore, ThreadStore};
27
28use crate::acp::message_editor::MessageEditor;
29use crate::context_picker::file_context_picker::{FileMatch, search_files};
30use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules};
31use crate::context_picker::symbol_context_picker::SymbolMatch;
32use crate::context_picker::symbol_context_picker::search_symbols;
33use crate::context_picker::thread_context_picker::{
34 ThreadContextEntry, ThreadMatch, search_threads,
35};
36use crate::context_picker::{
37 ContextPickerAction, ContextPickerEntry, ContextPickerMode, RecentEntry,
38 available_context_picker_entries, recent_context_picker_entries, selection_ranges,
39};
40
41#[derive(Clone, Debug, Eq, PartialEq)]
42pub struct MentionImage {
43 pub abs_path: Option<PathBuf>,
44 pub data: SharedString,
45 pub format: ImageFormat,
46}
47
48#[derive(Default)]
49pub struct MentionSet {
50 pub(crate) uri_by_crease_id: HashMap<CreaseId, MentionUri>,
51 fetch_results: HashMap<Url, Shared<Task<Result<String, String>>>>,
52 images: HashMap<CreaseId, Shared<Task<Result<MentionImage, String>>>>,
53}
54
55impl MentionSet {
56 pub fn insert_uri(&mut self, crease_id: CreaseId, uri: MentionUri) {
57 self.uri_by_crease_id.insert(crease_id, uri);
58 }
59
60 pub fn add_fetch_result(&mut self, url: Url, content: Shared<Task<Result<String, String>>>) {
61 self.fetch_results.insert(url, content);
62 }
63
64 pub fn insert_image(
65 &mut self,
66 crease_id: CreaseId,
67 task: Shared<Task<Result<MentionImage, String>>>,
68 ) {
69 self.images.insert(crease_id, task);
70 }
71
72 pub fn drain(&mut self) -> impl Iterator<Item = CreaseId> {
73 self.fetch_results.clear();
74 self.uri_by_crease_id
75 .drain()
76 .map(|(id, _)| id)
77 .chain(self.images.drain().map(|(id, _)| id))
78 }
79
80 pub fn contents(
81 &self,
82 project: Entity<Project>,
83 thread_store: Entity<ThreadStore>,
84 text_thread_store: Entity<TextThreadStore>,
85 window: &mut Window,
86 cx: &mut App,
87 ) -> Task<Result<HashMap<CreaseId, Mention>>> {
88 let mut processed_image_creases = HashSet::default();
89
90 let mut contents = self
91 .uri_by_crease_id
92 .iter()
93 .map(|(&crease_id, uri)| {
94 match uri {
95 MentionUri::File { abs_path, .. } => {
96 // TODO directories
97 let uri = uri.clone();
98 let abs_path = abs_path.to_path_buf();
99
100 if let Some(task) = self.images.get(&crease_id).cloned() {
101 processed_image_creases.insert(crease_id);
102 return cx.spawn(async move |_| {
103 let image = task.await.map_err(|e| anyhow!("{e}"))?;
104 anyhow::Ok((crease_id, Mention::Image(image)))
105 });
106 }
107
108 let buffer_task = project.update(cx, |project, cx| {
109 let path = project
110 .find_project_path(abs_path, cx)
111 .context("Failed to find project path")?;
112 anyhow::Ok(project.open_buffer(path, cx))
113 });
114 cx.spawn(async move |cx| {
115 let buffer = buffer_task?.await?;
116 let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
117
118 anyhow::Ok((crease_id, Mention::Text { uri, content }))
119 })
120 }
121 MentionUri::Symbol {
122 path, line_range, ..
123 }
124 | MentionUri::Selection {
125 path, line_range, ..
126 } => {
127 let uri = uri.clone();
128 let path_buf = path.clone();
129 let line_range = line_range.clone();
130
131 let buffer_task = project.update(cx, |project, cx| {
132 let path = project
133 .find_project_path(&path_buf, cx)
134 .context("Failed to find project path")?;
135 anyhow::Ok(project.open_buffer(path, cx))
136 });
137
138 cx.spawn(async move |cx| {
139 let buffer = buffer_task?.await?;
140 let content = buffer.read_with(cx, |buffer, _cx| {
141 buffer
142 .text_for_range(
143 Point::new(line_range.start, 0)
144 ..Point::new(
145 line_range.end,
146 buffer.line_len(line_range.end),
147 ),
148 )
149 .collect()
150 })?;
151
152 anyhow::Ok((crease_id, Mention::Text { uri, content }))
153 })
154 }
155 MentionUri::Thread { id: thread_id, .. } => {
156 let open_task = thread_store.update(cx, |thread_store, cx| {
157 thread_store.open_thread(&thread_id, window, cx)
158 });
159
160 let uri = uri.clone();
161 cx.spawn(async move |cx| {
162 let thread = open_task.await?;
163 let content = thread.read_with(cx, |thread, _cx| {
164 thread.latest_detailed_summary_or_text().to_string()
165 })?;
166
167 anyhow::Ok((crease_id, Mention::Text { uri, content }))
168 })
169 }
170 MentionUri::TextThread { path, .. } => {
171 let context = text_thread_store.update(cx, |text_thread_store, cx| {
172 text_thread_store.open_local_context(path.as_path().into(), cx)
173 });
174 let uri = uri.clone();
175 cx.spawn(async move |cx| {
176 let context = context.await?;
177 let xml = context.update(cx, |context, cx| context.to_xml(cx))?;
178 anyhow::Ok((crease_id, Mention::Text { uri, content: xml }))
179 })
180 }
181 MentionUri::Rule { id: prompt_id, .. } => {
182 let Some(prompt_store) = thread_store.read(cx).prompt_store().clone()
183 else {
184 return Task::ready(Err(anyhow!("missing prompt store")));
185 };
186 let text_task = prompt_store.read(cx).load(*prompt_id, cx);
187 let uri = uri.clone();
188 cx.spawn(async move |_| {
189 // TODO: report load errors instead of just logging
190 let text = text_task.await?;
191 anyhow::Ok((crease_id, Mention::Text { uri, content: text }))
192 })
193 }
194 MentionUri::Fetch { url } => {
195 let Some(content) = self.fetch_results.get(&url).cloned() else {
196 return Task::ready(Err(anyhow!("missing fetch result")));
197 };
198 let uri = uri.clone();
199 cx.spawn(async move |_| {
200 Ok((
201 crease_id,
202 Mention::Text {
203 uri,
204 content: content.await.map_err(|e| anyhow::anyhow!("{e}"))?,
205 },
206 ))
207 })
208 }
209 }
210 })
211 .collect::<Vec<_>>();
212
213 // Handle images that didn't have a mention URI (because they were added by the paste handler).
214 contents.extend(self.images.iter().filter_map(|(crease_id, image)| {
215 if processed_image_creases.contains(crease_id) {
216 return None;
217 }
218 let crease_id = *crease_id;
219 let image = image.clone();
220 Some(cx.spawn(async move |_| {
221 Ok((
222 crease_id,
223 Mention::Image(image.await.map_err(|e| anyhow::anyhow!("{e}"))?),
224 ))
225 }))
226 }));
227
228 cx.spawn(async move |_cx| {
229 let contents = try_join_all(contents).await?.into_iter().collect();
230 anyhow::Ok(contents)
231 })
232 }
233}
234
235#[derive(Debug, Eq, PartialEq)]
236pub enum Mention {
237 Text { uri: MentionUri, content: String },
238 Image(MentionImage),
239}
240
241pub(crate) enum Match {
242 File(FileMatch),
243 Symbol(SymbolMatch),
244 Thread(ThreadMatch),
245 Fetch(SharedString),
246 Rules(RulesContextEntry),
247 Entry(EntryMatch),
248}
249
250pub struct EntryMatch {
251 mat: Option<StringMatch>,
252 entry: ContextPickerEntry,
253}
254
255impl Match {
256 pub fn score(&self) -> f64 {
257 match self {
258 Match::File(file) => file.mat.score,
259 Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
260 Match::Thread(_) => 1.,
261 Match::Symbol(_) => 1.,
262 Match::Rules(_) => 1.,
263 Match::Fetch(_) => 1.,
264 }
265 }
266}
267
268fn search(
269 mode: Option<ContextPickerMode>,
270 query: String,
271 cancellation_flag: Arc<AtomicBool>,
272 recent_entries: Vec<RecentEntry>,
273 prompt_store: Option<Entity<PromptStore>>,
274 thread_store: WeakEntity<ThreadStore>,
275 text_thread_context_store: WeakEntity<assistant_context::ContextStore>,
276 workspace: Entity<Workspace>,
277 cx: &mut App,
278) -> Task<Vec<Match>> {
279 match mode {
280 Some(ContextPickerMode::File) => {
281 let search_files_task =
282 search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
283 cx.background_spawn(async move {
284 search_files_task
285 .await
286 .into_iter()
287 .map(Match::File)
288 .collect()
289 })
290 }
291
292 Some(ContextPickerMode::Symbol) => {
293 let search_symbols_task =
294 search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx);
295 cx.background_spawn(async move {
296 search_symbols_task
297 .await
298 .into_iter()
299 .map(Match::Symbol)
300 .collect()
301 })
302 }
303
304 Some(ContextPickerMode::Thread) => {
305 if let Some((thread_store, context_store)) = thread_store
306 .upgrade()
307 .zip(text_thread_context_store.upgrade())
308 {
309 let search_threads_task = search_threads(
310 query.clone(),
311 cancellation_flag.clone(),
312 thread_store,
313 context_store,
314 cx,
315 );
316 cx.background_spawn(async move {
317 search_threads_task
318 .await
319 .into_iter()
320 .map(Match::Thread)
321 .collect()
322 })
323 } else {
324 Task::ready(Vec::new())
325 }
326 }
327
328 Some(ContextPickerMode::Fetch) => {
329 if !query.is_empty() {
330 Task::ready(vec![Match::Fetch(query.into())])
331 } else {
332 Task::ready(Vec::new())
333 }
334 }
335
336 Some(ContextPickerMode::Rules) => {
337 if let Some(prompt_store) = prompt_store.as_ref() {
338 let search_rules_task =
339 search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx);
340 cx.background_spawn(async move {
341 search_rules_task
342 .await
343 .into_iter()
344 .map(Match::Rules)
345 .collect::<Vec<_>>()
346 })
347 } else {
348 Task::ready(Vec::new())
349 }
350 }
351
352 None => {
353 if query.is_empty() {
354 let mut matches = recent_entries
355 .into_iter()
356 .map(|entry| match entry {
357 RecentEntry::File {
358 project_path,
359 path_prefix,
360 } => Match::File(FileMatch {
361 mat: fuzzy::PathMatch {
362 score: 1.,
363 positions: Vec::new(),
364 worktree_id: project_path.worktree_id.to_usize(),
365 path: project_path.path,
366 path_prefix,
367 is_dir: false,
368 distance_to_relative_ancestor: 0,
369 },
370 is_recent: true,
371 }),
372 RecentEntry::Thread(thread_context_entry) => Match::Thread(ThreadMatch {
373 thread: thread_context_entry,
374 is_recent: true,
375 }),
376 })
377 .collect::<Vec<_>>();
378
379 matches.extend(
380 available_context_picker_entries(
381 &prompt_store,
382 &Some(thread_store.clone()),
383 &workspace,
384 cx,
385 )
386 .into_iter()
387 .map(|mode| {
388 Match::Entry(EntryMatch {
389 entry: mode,
390 mat: None,
391 })
392 }),
393 );
394
395 Task::ready(matches)
396 } else {
397 let executor = cx.background_executor().clone();
398
399 let search_files_task =
400 search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
401
402 let entries = available_context_picker_entries(
403 &prompt_store,
404 &Some(thread_store.clone()),
405 &workspace,
406 cx,
407 );
408 let entry_candidates = entries
409 .iter()
410 .enumerate()
411 .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword()))
412 .collect::<Vec<_>>();
413
414 cx.background_spawn(async move {
415 let mut matches = search_files_task
416 .await
417 .into_iter()
418 .map(Match::File)
419 .collect::<Vec<_>>();
420
421 let entry_matches = fuzzy::match_strings(
422 &entry_candidates,
423 &query,
424 false,
425 true,
426 100,
427 &Arc::new(AtomicBool::default()),
428 executor,
429 )
430 .await;
431
432 matches.extend(entry_matches.into_iter().map(|mat| {
433 Match::Entry(EntryMatch {
434 entry: entries[mat.candidate_id],
435 mat: Some(mat),
436 })
437 }));
438
439 matches.sort_by(|a, b| {
440 b.score()
441 .partial_cmp(&a.score())
442 .unwrap_or(std::cmp::Ordering::Equal)
443 });
444
445 matches
446 })
447 }
448 }
449 }
450}
451
452pub struct ContextPickerCompletionProvider {
453 workspace: WeakEntity<Workspace>,
454 thread_store: WeakEntity<ThreadStore>,
455 text_thread_store: WeakEntity<TextThreadStore>,
456 message_editor: WeakEntity<MessageEditor>,
457}
458
459impl ContextPickerCompletionProvider {
460 pub fn new(
461 workspace: WeakEntity<Workspace>,
462 thread_store: WeakEntity<ThreadStore>,
463 text_thread_store: WeakEntity<TextThreadStore>,
464 message_editor: WeakEntity<MessageEditor>,
465 ) -> Self {
466 Self {
467 workspace,
468 thread_store,
469 text_thread_store,
470 message_editor,
471 }
472 }
473
474 fn completion_for_entry(
475 entry: ContextPickerEntry,
476 source_range: Range<Anchor>,
477 message_editor: WeakEntity<MessageEditor>,
478 workspace: &Entity<Workspace>,
479 cx: &mut App,
480 ) -> Option<Completion> {
481 match entry {
482 ContextPickerEntry::Mode(mode) => Some(Completion {
483 replace_range: source_range.clone(),
484 new_text: format!("@{} ", mode.keyword()),
485 label: CodeLabel::plain(mode.label().to_string(), None),
486 icon_path: Some(mode.icon().path().into()),
487 documentation: None,
488 source: project::CompletionSource::Custom,
489 insert_text_mode: None,
490 // This ensures that when a user accepts this completion, the
491 // completion menu will still be shown after "@category " is
492 // inserted
493 confirm: Some(Arc::new(|_, _, _| true)),
494 }),
495 ContextPickerEntry::Action(action) => {
496 let (new_text, on_action) = match action {
497 ContextPickerAction::AddSelections => {
498 const PLACEHOLDER: &str = "selection ";
499 let selections = selection_ranges(workspace, cx)
500 .into_iter()
501 .enumerate()
502 .map(|(ix, (buffer, range))| {
503 (
504 buffer,
505 range,
506 (PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1),
507 )
508 })
509 .collect::<Vec<_>>();
510
511 let new_text: String = PLACEHOLDER.repeat(selections.len());
512
513 let callback = Arc::new({
514 let source_range = source_range.clone();
515 move |_, window: &mut Window, cx: &mut App| {
516 let selections = selections.clone();
517 let message_editor = message_editor.clone();
518 let source_range = source_range.clone();
519 window.defer(cx, move |window, cx| {
520 message_editor
521 .update(cx, |message_editor, cx| {
522 message_editor.confirm_mention_for_selection(
523 source_range,
524 selections,
525 window,
526 cx,
527 )
528 })
529 .ok();
530 });
531 false
532 }
533 });
534
535 (new_text, callback)
536 }
537 };
538
539 Some(Completion {
540 replace_range: source_range.clone(),
541 new_text,
542 label: CodeLabel::plain(action.label().to_string(), None),
543 icon_path: Some(action.icon().path().into()),
544 documentation: None,
545 source: project::CompletionSource::Custom,
546 insert_text_mode: None,
547 // This ensures that when a user accepts this completion, the
548 // completion menu will still be shown after "@category " is
549 // inserted
550 confirm: Some(on_action),
551 })
552 }
553 }
554 }
555
556 fn completion_for_thread(
557 thread_entry: ThreadContextEntry,
558 source_range: Range<Anchor>,
559 recent: bool,
560 editor: WeakEntity<MessageEditor>,
561 cx: &mut App,
562 ) -> Completion {
563 let uri = match &thread_entry {
564 ThreadContextEntry::Thread { id, title } => MentionUri::Thread {
565 id: id.clone(),
566 name: title.to_string(),
567 },
568 ThreadContextEntry::Context { path, title } => MentionUri::TextThread {
569 path: path.to_path_buf(),
570 name: title.to_string(),
571 },
572 };
573
574 let icon_for_completion = if recent {
575 IconName::HistoryRerun.path().into()
576 } else {
577 uri.icon_path(cx)
578 };
579
580 let new_text = format!("{} ", uri.as_link());
581
582 let new_text_len = new_text.len();
583 Completion {
584 replace_range: source_range.clone(),
585 new_text,
586 label: CodeLabel::plain(thread_entry.title().to_string(), None),
587 documentation: None,
588 insert_text_mode: None,
589 source: project::CompletionSource::Custom,
590 icon_path: Some(icon_for_completion.clone()),
591 confirm: Some(confirm_completion_callback(
592 thread_entry.title().clone(),
593 source_range.start,
594 new_text_len - 1,
595 editor,
596 uri,
597 )),
598 }
599 }
600
601 fn completion_for_rules(
602 rule: RulesContextEntry,
603 source_range: Range<Anchor>,
604 editor: WeakEntity<MessageEditor>,
605 cx: &mut App,
606 ) -> Completion {
607 let uri = MentionUri::Rule {
608 id: rule.prompt_id.into(),
609 name: rule.title.to_string(),
610 };
611 let new_text = format!("{} ", uri.as_link());
612 let new_text_len = new_text.len();
613 let icon_path = uri.icon_path(cx);
614 Completion {
615 replace_range: source_range.clone(),
616 new_text,
617 label: CodeLabel::plain(rule.title.to_string(), None),
618 documentation: None,
619 insert_text_mode: None,
620 source: project::CompletionSource::Custom,
621 icon_path: Some(icon_path.clone()),
622 confirm: Some(confirm_completion_callback(
623 rule.title.clone(),
624 source_range.start,
625 new_text_len - 1,
626 editor,
627 uri,
628 )),
629 }
630 }
631
632 pub(crate) fn completion_for_path(
633 project_path: ProjectPath,
634 path_prefix: &str,
635 is_recent: bool,
636 is_directory: bool,
637 source_range: Range<Anchor>,
638 message_editor: WeakEntity<MessageEditor>,
639 project: Entity<Project>,
640 cx: &mut App,
641 ) -> Option<Completion> {
642 let (file_name, directory) =
643 crate::context_picker::file_context_picker::extract_file_name_and_directory(
644 &project_path.path,
645 path_prefix,
646 );
647
648 let label =
649 build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
650
651 let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
652
653 let file_uri = MentionUri::File {
654 abs_path,
655 is_directory,
656 };
657
658 let crease_icon_path = file_uri.icon_path(cx);
659 let completion_icon_path = if is_recent {
660 IconName::HistoryRerun.path().into()
661 } else {
662 crease_icon_path.clone()
663 };
664
665 let new_text = format!("{} ", file_uri.as_link());
666 let new_text_len = new_text.len();
667 Some(Completion {
668 replace_range: source_range.clone(),
669 new_text,
670 label,
671 documentation: None,
672 source: project::CompletionSource::Custom,
673 icon_path: Some(completion_icon_path),
674 insert_text_mode: None,
675 confirm: Some(confirm_completion_callback(
676 file_name,
677 source_range.start,
678 new_text_len - 1,
679 message_editor,
680 file_uri,
681 )),
682 })
683 }
684
685 fn completion_for_symbol(
686 symbol: Symbol,
687 source_range: Range<Anchor>,
688 message_editor: WeakEntity<MessageEditor>,
689 workspace: Entity<Workspace>,
690 cx: &mut App,
691 ) -> Option<Completion> {
692 let project = workspace.read(cx).project().clone();
693
694 let label = CodeLabel::plain(symbol.name.clone(), None);
695
696 let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?;
697 let uri = MentionUri::Symbol {
698 path: abs_path,
699 name: symbol.name.clone(),
700 line_range: symbol.range.start.0.row..symbol.range.end.0.row,
701 };
702 let new_text = format!("{} ", uri.as_link());
703 let new_text_len = new_text.len();
704 let icon_path = uri.icon_path(cx);
705 Some(Completion {
706 replace_range: source_range.clone(),
707 new_text,
708 label,
709 documentation: None,
710 source: project::CompletionSource::Custom,
711 icon_path: Some(icon_path.clone()),
712 insert_text_mode: None,
713 confirm: Some(confirm_completion_callback(
714 symbol.name.clone().into(),
715 source_range.start,
716 new_text_len - 1,
717 message_editor,
718 uri,
719 )),
720 })
721 }
722
723 fn completion_for_fetch(
724 source_range: Range<Anchor>,
725 url_to_fetch: SharedString,
726 message_editor: WeakEntity<MessageEditor>,
727 cx: &mut App,
728 ) -> Option<Completion> {
729 let new_text = format!("@fetch {} ", url_to_fetch.clone());
730 let url_to_fetch = url::Url::parse(url_to_fetch.as_ref())
731 .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}")))
732 .ok()?;
733 let mention_uri = MentionUri::Fetch {
734 url: url_to_fetch.clone(),
735 };
736 let icon_path = mention_uri.icon_path(cx);
737 Some(Completion {
738 replace_range: source_range.clone(),
739 new_text: new_text.clone(),
740 label: CodeLabel::plain(url_to_fetch.to_string(), None),
741 documentation: None,
742 source: project::CompletionSource::Custom,
743 icon_path: Some(icon_path.clone()),
744 insert_text_mode: None,
745 confirm: Some(confirm_completion_callback(
746 url_to_fetch.to_string().into(),
747 source_range.start,
748 new_text.len() - 1,
749 message_editor,
750 mention_uri,
751 )),
752 })
753 }
754}
755
756fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
757 let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
758 let mut label = CodeLabel::default();
759
760 label.push_str(&file_name, None);
761 label.push_str(" ", None);
762
763 if let Some(directory) = directory {
764 label.push_str(&directory, comment_id);
765 }
766
767 label.filter_range = 0..label.text().len();
768
769 label
770}
771
772impl CompletionProvider for ContextPickerCompletionProvider {
773 fn completions(
774 &self,
775 _excerpt_id: ExcerptId,
776 buffer: &Entity<Buffer>,
777 buffer_position: Anchor,
778 _trigger: CompletionContext,
779 _window: &mut Window,
780 cx: &mut Context<Editor>,
781 ) -> Task<Result<Vec<CompletionResponse>>> {
782 let state = buffer.update(cx, |buffer, _cx| {
783 let position = buffer_position.to_point(buffer);
784 let line_start = Point::new(position.row, 0);
785 let offset_to_line = buffer.point_to_offset(line_start);
786 let mut lines = buffer.text_for_range(line_start..position).lines();
787 let line = lines.next()?;
788 MentionCompletion::try_parse(line, offset_to_line)
789 });
790 let Some(state) = state else {
791 return Task::ready(Ok(Vec::new()));
792 };
793
794 let Some(workspace) = self.workspace.upgrade() else {
795 return Task::ready(Ok(Vec::new()));
796 };
797
798 let project = workspace.read(cx).project().clone();
799 let snapshot = buffer.read(cx).snapshot();
800 let source_range = snapshot.anchor_before(state.source_range.start)
801 ..snapshot.anchor_after(state.source_range.end);
802
803 let thread_store = self.thread_store.clone();
804 let text_thread_store = self.text_thread_store.clone();
805 let editor = self.message_editor.clone();
806 let Ok((exclude_paths, exclude_threads)) =
807 self.message_editor.update(cx, |message_editor, _cx| {
808 message_editor.mentioned_path_and_threads()
809 })
810 else {
811 return Task::ready(Ok(Vec::new()));
812 };
813
814 let MentionCompletion { mode, argument, .. } = state;
815 let query = argument.unwrap_or_else(|| "".to_string());
816
817 let recent_entries = recent_context_picker_entries(
818 Some(thread_store.clone()),
819 Some(text_thread_store.clone()),
820 workspace.clone(),
821 &exclude_paths,
822 &exclude_threads,
823 cx,
824 );
825
826 let prompt_store = thread_store
827 .read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone())
828 .ok()
829 .flatten();
830
831 let search_task = search(
832 mode,
833 query,
834 Arc::<AtomicBool>::default(),
835 recent_entries,
836 prompt_store,
837 thread_store.clone(),
838 text_thread_store.clone(),
839 workspace.clone(),
840 cx,
841 );
842
843 cx.spawn(async move |_, cx| {
844 let matches = search_task.await;
845
846 let completions = cx.update(|cx| {
847 matches
848 .into_iter()
849 .filter_map(|mat| match mat {
850 Match::File(FileMatch { mat, is_recent }) => {
851 let project_path = ProjectPath {
852 worktree_id: WorktreeId::from_usize(mat.worktree_id),
853 path: mat.path.clone(),
854 };
855
856 Self::completion_for_path(
857 project_path,
858 &mat.path_prefix,
859 is_recent,
860 mat.is_dir,
861 source_range.clone(),
862 editor.clone(),
863 project.clone(),
864 cx,
865 )
866 }
867
868 Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
869 symbol,
870 source_range.clone(),
871 editor.clone(),
872 workspace.clone(),
873 cx,
874 ),
875
876 Match::Thread(ThreadMatch {
877 thread, is_recent, ..
878 }) => Some(Self::completion_for_thread(
879 thread,
880 source_range.clone(),
881 is_recent,
882 editor.clone(),
883 cx,
884 )),
885
886 Match::Rules(user_rules) => Some(Self::completion_for_rules(
887 user_rules,
888 source_range.clone(),
889 editor.clone(),
890 cx,
891 )),
892
893 Match::Fetch(url) => Self::completion_for_fetch(
894 source_range.clone(),
895 url,
896 editor.clone(),
897 cx,
898 ),
899
900 Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
901 entry,
902 source_range.clone(),
903 editor.clone(),
904 &workspace,
905 cx,
906 ),
907 })
908 .collect()
909 })?;
910
911 Ok(vec![CompletionResponse {
912 completions,
913 // Since this does its own filtering (see `filter_completions()` returns false),
914 // there is no benefit to computing whether this set of completions is incomplete.
915 is_incomplete: true,
916 }])
917 })
918 }
919
920 fn is_completion_trigger(
921 &self,
922 buffer: &Entity<language::Buffer>,
923 position: language::Anchor,
924 _text: &str,
925 _trigger_in_words: bool,
926 _menu_is_open: bool,
927 cx: &mut Context<Editor>,
928 ) -> bool {
929 let buffer = buffer.read(cx);
930 let position = position.to_point(buffer);
931 let line_start = Point::new(position.row, 0);
932 let offset_to_line = buffer.point_to_offset(line_start);
933 let mut lines = buffer.text_for_range(line_start..position).lines();
934 if let Some(line) = lines.next() {
935 MentionCompletion::try_parse(line, offset_to_line)
936 .map(|completion| {
937 completion.source_range.start <= offset_to_line + position.column as usize
938 && completion.source_range.end >= offset_to_line + position.column as usize
939 })
940 .unwrap_or(false)
941 } else {
942 false
943 }
944 }
945
946 fn sort_completions(&self) -> bool {
947 false
948 }
949
950 fn filter_completions(&self) -> bool {
951 false
952 }
953}
954
955fn confirm_completion_callback(
956 crease_text: SharedString,
957 start: Anchor,
958 content_len: usize,
959 message_editor: WeakEntity<MessageEditor>,
960 mention_uri: MentionUri,
961) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
962 Arc::new(move |_, window, cx| {
963 let message_editor = message_editor.clone();
964 let crease_text = crease_text.clone();
965 let mention_uri = mention_uri.clone();
966 window.defer(cx, move |window, cx| {
967 message_editor
968 .clone()
969 .update(cx, |message_editor, cx| {
970 message_editor.confirm_completion(
971 crease_text,
972 start,
973 content_len,
974 mention_uri,
975 window,
976 cx,
977 )
978 })
979 .ok();
980 });
981 false
982 })
983}
984
985#[derive(Debug, Default, PartialEq)]
986struct MentionCompletion {
987 source_range: Range<usize>,
988 mode: Option<ContextPickerMode>,
989 argument: Option<String>,
990}
991
992impl MentionCompletion {
993 fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
994 let last_mention_start = line.rfind('@')?;
995 if last_mention_start >= line.len() {
996 return Some(Self::default());
997 }
998 if last_mention_start > 0
999 && line
1000 .chars()
1001 .nth(last_mention_start - 1)
1002 .map_or(false, |c| !c.is_whitespace())
1003 {
1004 return None;
1005 }
1006
1007 let rest_of_line = &line[last_mention_start + 1..];
1008
1009 let mut mode = None;
1010 let mut argument = None;
1011
1012 let mut parts = rest_of_line.split_whitespace();
1013 let mut end = last_mention_start + 1;
1014 if let Some(mode_text) = parts.next() {
1015 end += mode_text.len();
1016
1017 if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok() {
1018 mode = Some(parsed_mode);
1019 } else {
1020 argument = Some(mode_text.to_string());
1021 }
1022 match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
1023 Some(whitespace_count) => {
1024 if let Some(argument_text) = parts.next() {
1025 argument = Some(argument_text.to_string());
1026 end += whitespace_count + argument_text.len();
1027 }
1028 }
1029 None => {
1030 // Rest of line is entirely whitespace
1031 end += rest_of_line.len() - mode_text.len();
1032 }
1033 }
1034 }
1035
1036 Some(Self {
1037 source_range: last_mention_start + offset_to_line..end + offset_to_line,
1038 mode,
1039 argument,
1040 })
1041 }
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046 use super::*;
1047 use editor::{AnchorRangeExt, EditorMode};
1048 use gpui::{EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext};
1049 use project::{Project, ProjectPath};
1050 use serde_json::json;
1051 use settings::SettingsStore;
1052 use smol::stream::StreamExt as _;
1053 use std::{ops::Deref, path::Path};
1054 use util::path;
1055 use workspace::{AppState, Item};
1056
1057 #[test]
1058 fn test_mention_completion_parse() {
1059 assert_eq!(MentionCompletion::try_parse("Lorem Ipsum", 0), None);
1060
1061 assert_eq!(
1062 MentionCompletion::try_parse("Lorem @", 0),
1063 Some(MentionCompletion {
1064 source_range: 6..7,
1065 mode: None,
1066 argument: None,
1067 })
1068 );
1069
1070 assert_eq!(
1071 MentionCompletion::try_parse("Lorem @file", 0),
1072 Some(MentionCompletion {
1073 source_range: 6..11,
1074 mode: Some(ContextPickerMode::File),
1075 argument: None,
1076 })
1077 );
1078
1079 assert_eq!(
1080 MentionCompletion::try_parse("Lorem @file ", 0),
1081 Some(MentionCompletion {
1082 source_range: 6..12,
1083 mode: Some(ContextPickerMode::File),
1084 argument: None,
1085 })
1086 );
1087
1088 assert_eq!(
1089 MentionCompletion::try_parse("Lorem @file main.rs", 0),
1090 Some(MentionCompletion {
1091 source_range: 6..19,
1092 mode: Some(ContextPickerMode::File),
1093 argument: Some("main.rs".to_string()),
1094 })
1095 );
1096
1097 assert_eq!(
1098 MentionCompletion::try_parse("Lorem @file main.rs ", 0),
1099 Some(MentionCompletion {
1100 source_range: 6..19,
1101 mode: Some(ContextPickerMode::File),
1102 argument: Some("main.rs".to_string()),
1103 })
1104 );
1105
1106 assert_eq!(
1107 MentionCompletion::try_parse("Lorem @file main.rs Ipsum", 0),
1108 Some(MentionCompletion {
1109 source_range: 6..19,
1110 mode: Some(ContextPickerMode::File),
1111 argument: Some("main.rs".to_string()),
1112 })
1113 );
1114
1115 assert_eq!(
1116 MentionCompletion::try_parse("Lorem @main", 0),
1117 Some(MentionCompletion {
1118 source_range: 6..11,
1119 mode: None,
1120 argument: Some("main".to_string()),
1121 })
1122 );
1123
1124 assert_eq!(MentionCompletion::try_parse("test@", 0), None);
1125 }
1126
1127 struct MessageEditorItem(Entity<MessageEditor>);
1128
1129 impl Item for MessageEditorItem {
1130 type Event = ();
1131
1132 fn include_in_nav_history() -> bool {
1133 false
1134 }
1135
1136 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1137 "Test".into()
1138 }
1139 }
1140
1141 impl EventEmitter<()> for MessageEditorItem {}
1142
1143 impl Focusable for MessageEditorItem {
1144 fn focus_handle(&self, cx: &App) -> FocusHandle {
1145 self.0.read(cx).focus_handle(cx).clone()
1146 }
1147 }
1148
1149 impl Render for MessageEditorItem {
1150 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1151 self.0.clone().into_any_element()
1152 }
1153 }
1154
1155 #[gpui::test]
1156 async fn test_context_completion_provider(cx: &mut TestAppContext) {
1157 init_test(cx);
1158
1159 let app_state = cx.update(AppState::test);
1160
1161 cx.update(|cx| {
1162 language::init(cx);
1163 editor::init(cx);
1164 workspace::init(app_state.clone(), cx);
1165 Project::init_settings(cx);
1166 });
1167
1168 app_state
1169 .fs
1170 .as_fake()
1171 .insert_tree(
1172 path!("/dir"),
1173 json!({
1174 "editor": "",
1175 "a": {
1176 "one.txt": "1",
1177 "two.txt": "2",
1178 "three.txt": "3",
1179 "four.txt": "4"
1180 },
1181 "b": {
1182 "five.txt": "5",
1183 "six.txt": "6",
1184 "seven.txt": "7",
1185 "eight.txt": "8",
1186 }
1187 }),
1188 )
1189 .await;
1190
1191 let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
1192 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1193 let workspace = window.root(cx).unwrap();
1194
1195 let worktree = project.update(cx, |project, cx| {
1196 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
1197 assert_eq!(worktrees.len(), 1);
1198 worktrees.pop().unwrap()
1199 });
1200 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1201
1202 let mut cx = VisualTestContext::from_window(*window.deref(), cx);
1203
1204 let paths = vec![
1205 path!("a/one.txt"),
1206 path!("a/two.txt"),
1207 path!("a/three.txt"),
1208 path!("a/four.txt"),
1209 path!("b/five.txt"),
1210 path!("b/six.txt"),
1211 path!("b/seven.txt"),
1212 path!("b/eight.txt"),
1213 ];
1214
1215 let mut opened_editors = Vec::new();
1216 for path in paths {
1217 let buffer = workspace
1218 .update_in(&mut cx, |workspace, window, cx| {
1219 workspace.open_path(
1220 ProjectPath {
1221 worktree_id,
1222 path: Path::new(path).into(),
1223 },
1224 None,
1225 false,
1226 window,
1227 cx,
1228 )
1229 })
1230 .await
1231 .unwrap();
1232 opened_editors.push(buffer);
1233 }
1234
1235 let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx));
1236 let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
1237
1238 let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
1239 let workspace_handle = cx.weak_entity();
1240 let message_editor = cx.new(|cx| {
1241 MessageEditor::new(
1242 workspace_handle,
1243 project.clone(),
1244 thread_store.clone(),
1245 text_thread_store.clone(),
1246 EditorMode::AutoHeight {
1247 max_lines: None,
1248 min_lines: 1,
1249 },
1250 window,
1251 cx,
1252 )
1253 });
1254 workspace.active_pane().update(cx, |pane, cx| {
1255 pane.add_item(
1256 Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
1257 true,
1258 true,
1259 None,
1260 window,
1261 cx,
1262 );
1263 });
1264 message_editor.read(cx).focus_handle(cx).focus(window);
1265 let editor = message_editor.read(cx).editor().clone();
1266 (message_editor, editor)
1267 });
1268
1269 cx.simulate_input("Lorem ");
1270
1271 editor.update(&mut cx, |editor, cx| {
1272 assert_eq!(editor.text(cx), "Lorem ");
1273 assert!(!editor.has_visible_completions_menu());
1274 });
1275
1276 cx.simulate_input("@");
1277
1278 editor.update(&mut cx, |editor, cx| {
1279 assert_eq!(editor.text(cx), "Lorem @");
1280 assert!(editor.has_visible_completions_menu());
1281 assert_eq!(
1282 current_completion_labels(editor),
1283 &[
1284 "eight.txt dir/b/",
1285 "seven.txt dir/b/",
1286 "six.txt dir/b/",
1287 "five.txt dir/b/",
1288 "Files & Directories",
1289 "Symbols",
1290 "Threads",
1291 "Fetch"
1292 ]
1293 );
1294 });
1295
1296 // Select and confirm "File"
1297 editor.update_in(&mut cx, |editor, window, cx| {
1298 assert!(editor.has_visible_completions_menu());
1299 editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1300 editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1301 editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1302 editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
1303 editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1304 });
1305
1306 cx.run_until_parked();
1307
1308 editor.update(&mut cx, |editor, cx| {
1309 assert_eq!(editor.text(cx), "Lorem @file ");
1310 assert!(editor.has_visible_completions_menu());
1311 });
1312
1313 cx.simulate_input("one");
1314
1315 editor.update(&mut cx, |editor, cx| {
1316 assert_eq!(editor.text(cx), "Lorem @file one");
1317 assert!(editor.has_visible_completions_menu());
1318 assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
1319 });
1320
1321 editor.update_in(&mut cx, |editor, window, cx| {
1322 assert!(editor.has_visible_completions_menu());
1323 editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1324 });
1325
1326 editor.update(&mut cx, |editor, cx| {
1327 assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) ");
1328 assert!(!editor.has_visible_completions_menu());
1329 assert_eq!(
1330 fold_ranges(editor, cx),
1331 vec![Point::new(0, 6)..Point::new(0, 39)]
1332 );
1333 });
1334
1335 let contents = message_editor
1336 .update_in(&mut cx, |message_editor, window, cx| {
1337 message_editor.mention_set().contents(
1338 project.clone(),
1339 thread_store.clone(),
1340 text_thread_store.clone(),
1341 window,
1342 cx,
1343 )
1344 })
1345 .await
1346 .unwrap()
1347 .into_values()
1348 .collect::<Vec<_>>();
1349
1350 pretty_assertions::assert_eq!(
1351 contents,
1352 [Mention::Text {
1353 content: "1".into(),
1354 uri: "file:///dir/a/one.txt".parse().unwrap()
1355 }]
1356 );
1357
1358 cx.simulate_input(" ");
1359
1360 editor.update(&mut cx, |editor, cx| {
1361 assert_eq!(editor.text(cx), "Lorem [@one.txt](file:///dir/a/one.txt) ");
1362 assert!(!editor.has_visible_completions_menu());
1363 assert_eq!(
1364 fold_ranges(editor, cx),
1365 vec![Point::new(0, 6)..Point::new(0, 39)]
1366 );
1367 });
1368
1369 cx.simulate_input("Ipsum ");
1370
1371 editor.update(&mut cx, |editor, cx| {
1372 assert_eq!(
1373 editor.text(cx),
1374 "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum ",
1375 );
1376 assert!(!editor.has_visible_completions_menu());
1377 assert_eq!(
1378 fold_ranges(editor, cx),
1379 vec![Point::new(0, 6)..Point::new(0, 39)]
1380 );
1381 });
1382
1383 cx.simulate_input("@file ");
1384
1385 editor.update(&mut cx, |editor, cx| {
1386 assert_eq!(
1387 editor.text(cx),
1388 "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum @file ",
1389 );
1390 assert!(editor.has_visible_completions_menu());
1391 assert_eq!(
1392 fold_ranges(editor, cx),
1393 vec![Point::new(0, 6)..Point::new(0, 39)]
1394 );
1395 });
1396
1397 editor.update_in(&mut cx, |editor, window, cx| {
1398 editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1399 });
1400
1401 cx.run_until_parked();
1402
1403 let contents = message_editor
1404 .update_in(&mut cx, |message_editor, window, cx| {
1405 message_editor.mention_set().contents(
1406 project.clone(),
1407 thread_store.clone(),
1408 text_thread_store.clone(),
1409 window,
1410 cx,
1411 )
1412 })
1413 .await
1414 .unwrap()
1415 .into_values()
1416 .collect::<Vec<_>>();
1417
1418 assert_eq!(contents.len(), 2);
1419 pretty_assertions::assert_eq!(
1420 contents[1],
1421 Mention::Text {
1422 content: "8".to_string(),
1423 uri: "file:///dir/b/eight.txt".parse().unwrap(),
1424 }
1425 );
1426
1427 editor.update(&mut cx, |editor, cx| {
1428 assert_eq!(
1429 editor.text(cx),
1430 "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) "
1431 );
1432 assert!(!editor.has_visible_completions_menu());
1433 assert_eq!(
1434 fold_ranges(editor, cx),
1435 vec![
1436 Point::new(0, 6)..Point::new(0, 39),
1437 Point::new(0, 47)..Point::new(0, 84)
1438 ]
1439 );
1440 });
1441
1442 let plain_text_language = Arc::new(language::Language::new(
1443 language::LanguageConfig {
1444 name: "Plain Text".into(),
1445 matcher: language::LanguageMatcher {
1446 path_suffixes: vec!["txt".to_string()],
1447 ..Default::default()
1448 },
1449 ..Default::default()
1450 },
1451 None,
1452 ));
1453
1454 // Register the language and fake LSP
1455 let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
1456 language_registry.add(plain_text_language);
1457
1458 let mut fake_language_servers = language_registry.register_fake_lsp(
1459 "Plain Text",
1460 language::FakeLspAdapter {
1461 capabilities: lsp::ServerCapabilities {
1462 workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
1463 ..Default::default()
1464 },
1465 ..Default::default()
1466 },
1467 );
1468
1469 // Open the buffer to trigger LSP initialization
1470 let buffer = project
1471 .update(&mut cx, |project, cx| {
1472 project.open_local_buffer(path!("/dir/a/one.txt"), cx)
1473 })
1474 .await
1475 .unwrap();
1476
1477 // Register the buffer with language servers
1478 let _handle = project.update(&mut cx, |project, cx| {
1479 project.register_buffer_with_language_servers(&buffer, cx)
1480 });
1481
1482 cx.run_until_parked();
1483
1484 let fake_language_server = fake_language_servers.next().await.unwrap();
1485 fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
1486 |_, _| async move {
1487 Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
1488 #[allow(deprecated)]
1489 lsp::SymbolInformation {
1490 name: "MySymbol".into(),
1491 location: lsp::Location {
1492 uri: lsp::Url::from_file_path(path!("/dir/a/one.txt")).unwrap(),
1493 range: lsp::Range::new(
1494 lsp::Position::new(0, 0),
1495 lsp::Position::new(0, 1),
1496 ),
1497 },
1498 kind: lsp::SymbolKind::CONSTANT,
1499 tags: None,
1500 container_name: None,
1501 deprecated: None,
1502 },
1503 ])))
1504 },
1505 );
1506
1507 cx.simulate_input("@symbol ");
1508
1509 editor.update(&mut cx, |editor, cx| {
1510 assert_eq!(
1511 editor.text(cx),
1512 "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) @symbol "
1513 );
1514 assert!(editor.has_visible_completions_menu());
1515 assert_eq!(
1516 current_completion_labels(editor),
1517 &[
1518 "MySymbol",
1519 ]
1520 );
1521 });
1522
1523 editor.update_in(&mut cx, |editor, window, cx| {
1524 editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
1525 });
1526
1527 let contents = message_editor
1528 .update_in(&mut cx, |message_editor, window, cx| {
1529 message_editor.mention_set().contents(
1530 project.clone(),
1531 thread_store,
1532 text_thread_store,
1533 window,
1534 cx,
1535 )
1536 })
1537 .await
1538 .unwrap()
1539 .into_values()
1540 .collect::<Vec<_>>();
1541
1542 assert_eq!(contents.len(), 3);
1543 pretty_assertions::assert_eq!(
1544 contents[2],
1545 Mention::Text {
1546 content: "1".into(),
1547 uri: "file:///dir/a/one.txt?symbol=MySymbol#L1:1"
1548 .parse()
1549 .unwrap(),
1550 }
1551 );
1552
1553 cx.run_until_parked();
1554
1555 editor.read_with(&mut cx, |editor, cx| {
1556 assert_eq!(
1557 editor.text(cx),
1558 "Lorem [@one.txt](file:///dir/a/one.txt) Ipsum [@eight.txt](file:///dir/b/eight.txt) [@MySymbol](file:///dir/a/one.txt?symbol=MySymbol#L1:1) "
1559 );
1560 });
1561 }
1562
1563 fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
1564 let snapshot = editor.buffer().read(cx).snapshot(cx);
1565 editor.display_map.update(cx, |display_map, cx| {
1566 display_map
1567 .snapshot(cx)
1568 .folds_in_range(0..snapshot.len())
1569 .map(|fold| fold.range.to_point(&snapshot))
1570 .collect()
1571 })
1572 }
1573
1574 fn current_completion_labels(editor: &Editor) -> Vec<String> {
1575 let completions = editor.current_completions().expect("Missing completions");
1576 completions
1577 .into_iter()
1578 .map(|completion| completion.label.text.to_string())
1579 .collect::<Vec<_>>()
1580 }
1581
1582 pub(crate) fn init_test(cx: &mut TestAppContext) {
1583 cx.update(|cx| {
1584 let store = SettingsStore::test(cx);
1585 cx.set_global(store);
1586 theme::init(theme::LoadThemes::JustBase, cx);
1587 client::init_settings(cx);
1588 language::init(cx);
1589 Project::init_settings(cx);
1590 workspace::init_settings(cx);
1591 editor::init_settings(cx);
1592 });
1593 }
1594}