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