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