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