1mod completion_provider;
2mod fetch_context_picker;
3mod file_context_picker;
4mod rules_context_picker;
5mod symbol_context_picker;
6mod thread_context_picker;
7
8use std::ops::Range;
9use std::path::PathBuf;
10use std::sync::Arc;
11
12use anyhow::{Result, anyhow};
13use editor::display_map::{Crease, FoldId};
14use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
15use file_context_picker::render_file_context_entry;
16use gpui::{
17 App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
18 WeakEntity,
19};
20use language::Buffer;
21use multi_buffer::MultiBufferRow;
22use project::{Entry, ProjectPath};
23use prompt_store::UserPromptId;
24use rules_context_picker::RulesContextEntry;
25use symbol_context_picker::SymbolContextPicker;
26use thread_context_picker::{ThreadContextEntry, render_thread_context_entry};
27use ui::{
28 ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
29};
30use uuid::Uuid;
31use workspace::{Workspace, notifications::NotifyResultExt};
32
33use crate::AssistantPanel;
34use crate::context::RULES_ICON;
35pub use crate::context_picker::completion_provider::ContextPickerCompletionProvider;
36use crate::context_picker::fetch_context_picker::FetchContextPicker;
37use crate::context_picker::file_context_picker::FileContextPicker;
38use crate::context_picker::rules_context_picker::RulesContextPicker;
39use crate::context_picker::thread_context_picker::ThreadContextPicker;
40use crate::context_store::ContextStore;
41use crate::thread::ThreadId;
42use crate::thread_store::ThreadStore;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45enum ContextPickerEntry {
46 Mode(ContextPickerMode),
47 Action(ContextPickerAction),
48}
49
50impl ContextPickerEntry {
51 pub fn keyword(&self) -> &'static str {
52 match self {
53 Self::Mode(mode) => mode.keyword(),
54 Self::Action(action) => action.keyword(),
55 }
56 }
57
58 pub fn label(&self) -> &'static str {
59 match self {
60 Self::Mode(mode) => mode.label(),
61 Self::Action(action) => action.label(),
62 }
63 }
64
65 pub fn icon(&self) -> IconName {
66 match self {
67 Self::Mode(mode) => mode.icon(),
68 Self::Action(action) => action.icon(),
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74enum ContextPickerMode {
75 File,
76 Symbol,
77 Fetch,
78 Thread,
79 Rules,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83enum ContextPickerAction {
84 AddSelections,
85}
86
87impl ContextPickerAction {
88 pub fn keyword(&self) -> &'static str {
89 match self {
90 Self::AddSelections => "selection",
91 }
92 }
93
94 pub fn label(&self) -> &'static str {
95 match self {
96 Self::AddSelections => "Selection",
97 }
98 }
99
100 pub fn icon(&self) -> IconName {
101 match self {
102 Self::AddSelections => IconName::Context,
103 }
104 }
105}
106
107impl TryFrom<&str> for ContextPickerMode {
108 type Error = String;
109
110 fn try_from(value: &str) -> Result<Self, Self::Error> {
111 match value {
112 "file" => Ok(Self::File),
113 "symbol" => Ok(Self::Symbol),
114 "fetch" => Ok(Self::Fetch),
115 "thread" => Ok(Self::Thread),
116 "rules" => Ok(Self::Rules),
117 _ => Err(format!("Invalid context picker mode: {}", value)),
118 }
119 }
120}
121
122impl ContextPickerMode {
123 pub fn keyword(&self) -> &'static str {
124 match self {
125 Self::File => "file",
126 Self::Symbol => "symbol",
127 Self::Fetch => "fetch",
128 Self::Thread => "thread",
129 Self::Rules => "rules",
130 }
131 }
132
133 pub fn label(&self) -> &'static str {
134 match self {
135 Self::File => "Files & Directories",
136 Self::Symbol => "Symbols",
137 Self::Fetch => "Fetch",
138 Self::Thread => "Threads",
139 Self::Rules => "Rules",
140 }
141 }
142
143 pub fn icon(&self) -> IconName {
144 match self {
145 Self::File => IconName::File,
146 Self::Symbol => IconName::Code,
147 Self::Fetch => IconName::Globe,
148 Self::Thread => IconName::MessageBubbles,
149 Self::Rules => RULES_ICON,
150 }
151 }
152}
153
154#[derive(Debug, Clone)]
155enum ContextPickerState {
156 Default(Entity<ContextMenu>),
157 File(Entity<FileContextPicker>),
158 Symbol(Entity<SymbolContextPicker>),
159 Fetch(Entity<FetchContextPicker>),
160 Thread(Entity<ThreadContextPicker>),
161 Rules(Entity<RulesContextPicker>),
162}
163
164pub(super) struct ContextPicker {
165 mode: ContextPickerState,
166 workspace: WeakEntity<Workspace>,
167 context_store: WeakEntity<ContextStore>,
168 thread_store: Option<WeakEntity<ThreadStore>>,
169 _subscriptions: Vec<Subscription>,
170}
171
172impl ContextPicker {
173 pub fn new(
174 workspace: WeakEntity<Workspace>,
175 thread_store: Option<WeakEntity<ThreadStore>>,
176 context_store: WeakEntity<ContextStore>,
177 window: &mut Window,
178 cx: &mut Context<Self>,
179 ) -> Self {
180 let subscriptions = context_store
181 .upgrade()
182 .map(|context_store| {
183 cx.observe(&context_store, |this, _, cx| this.notify_current_picker(cx))
184 })
185 .into_iter()
186 .chain(
187 thread_store
188 .as_ref()
189 .and_then(|thread_store| thread_store.upgrade())
190 .map(|thread_store| {
191 cx.observe(&thread_store, |this, _, cx| this.notify_current_picker(cx))
192 }),
193 )
194 .collect::<Vec<Subscription>>();
195
196 ContextPicker {
197 mode: ContextPickerState::Default(ContextMenu::build(
198 window,
199 cx,
200 |menu, _window, _cx| menu,
201 )),
202 workspace,
203 context_store,
204 thread_store,
205 _subscriptions: subscriptions,
206 }
207 }
208
209 pub fn init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
210 self.mode = ContextPickerState::Default(self.build_menu(window, cx));
211 cx.notify();
212 }
213
214 fn build_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<ContextMenu> {
215 let context_picker = cx.entity().clone();
216
217 let menu = ContextMenu::build(window, cx, move |menu, _window, cx| {
218 let recent = self.recent_entries(cx);
219 let has_recent = !recent.is_empty();
220 let recent_entries = recent
221 .into_iter()
222 .enumerate()
223 .map(|(ix, entry)| self.recent_menu_item(context_picker.clone(), ix, entry));
224
225 let entries = self
226 .workspace
227 .upgrade()
228 .map(|workspace| {
229 available_context_picker_entries(&self.thread_store, &workspace, cx)
230 })
231 .unwrap_or_default();
232
233 menu.when(has_recent, |menu| {
234 menu.custom_row(|_, _| {
235 div()
236 .mb_1()
237 .child(
238 Label::new("Recent")
239 .color(Color::Muted)
240 .size(LabelSize::Small),
241 )
242 .into_any_element()
243 })
244 })
245 .extend(recent_entries)
246 .when(has_recent, |menu| menu.separator())
247 .extend(entries.into_iter().map(|entry| {
248 let context_picker = context_picker.clone();
249
250 ContextMenuEntry::new(entry.label())
251 .icon(entry.icon())
252 .icon_size(IconSize::XSmall)
253 .icon_color(Color::Muted)
254 .handler(move |window, cx| {
255 context_picker.update(cx, |this, cx| this.select_entry(entry, window, cx))
256 })
257 }))
258 .keep_open_on_confirm()
259 });
260
261 cx.subscribe(&menu, move |_, _, _: &DismissEvent, cx| {
262 cx.emit(DismissEvent);
263 })
264 .detach();
265
266 menu
267 }
268
269 /// Whether threads are allowed as context.
270 pub fn allow_threads(&self) -> bool {
271 self.thread_store.is_some()
272 }
273
274 fn select_entry(
275 &mut self,
276 entry: ContextPickerEntry,
277 window: &mut Window,
278 cx: &mut Context<Self>,
279 ) {
280 let context_picker = cx.entity().downgrade();
281
282 match entry {
283 ContextPickerEntry::Mode(mode) => match mode {
284 ContextPickerMode::File => {
285 self.mode = ContextPickerState::File(cx.new(|cx| {
286 FileContextPicker::new(
287 context_picker.clone(),
288 self.workspace.clone(),
289 self.context_store.clone(),
290 window,
291 cx,
292 )
293 }));
294 }
295 ContextPickerMode::Symbol => {
296 self.mode = ContextPickerState::Symbol(cx.new(|cx| {
297 SymbolContextPicker::new(
298 context_picker.clone(),
299 self.workspace.clone(),
300 self.context_store.clone(),
301 window,
302 cx,
303 )
304 }));
305 }
306 ContextPickerMode::Rules => {
307 if let Some(thread_store) = self.thread_store.as_ref() {
308 self.mode = ContextPickerState::Rules(cx.new(|cx| {
309 RulesContextPicker::new(
310 thread_store.clone(),
311 context_picker.clone(),
312 self.context_store.clone(),
313 window,
314 cx,
315 )
316 }));
317 }
318 }
319 ContextPickerMode::Fetch => {
320 self.mode = ContextPickerState::Fetch(cx.new(|cx| {
321 FetchContextPicker::new(
322 context_picker.clone(),
323 self.workspace.clone(),
324 self.context_store.clone(),
325 window,
326 cx,
327 )
328 }));
329 }
330 ContextPickerMode::Thread => {
331 if let Some(thread_store) = self.thread_store.as_ref() {
332 self.mode = ContextPickerState::Thread(cx.new(|cx| {
333 ThreadContextPicker::new(
334 thread_store.clone(),
335 context_picker.clone(),
336 self.context_store.clone(),
337 window,
338 cx,
339 )
340 }));
341 }
342 }
343 },
344 ContextPickerEntry::Action(action) => match action {
345 ContextPickerAction::AddSelections => {
346 if let Some((context_store, workspace)) =
347 self.context_store.upgrade().zip(self.workspace.upgrade())
348 {
349 add_selections_as_context(&context_store, &workspace, cx);
350 }
351
352 cx.emit(DismissEvent);
353 }
354 },
355 }
356
357 cx.notify();
358 cx.focus_self(window);
359 }
360
361 fn recent_menu_item(
362 &self,
363 context_picker: Entity<ContextPicker>,
364 ix: usize,
365 entry: RecentEntry,
366 ) -> ContextMenuItem {
367 match entry {
368 RecentEntry::File {
369 project_path,
370 path_prefix,
371 } => {
372 let context_store = self.context_store.clone();
373 let worktree_id = project_path.worktree_id;
374 let path = project_path.path.clone();
375
376 ContextMenuItem::custom_entry(
377 move |_window, cx| {
378 render_file_context_entry(
379 ElementId::NamedInteger("ctx-recent".into(), ix),
380 worktree_id,
381 &path,
382 &path_prefix,
383 false,
384 context_store.clone(),
385 cx,
386 )
387 .into_any()
388 },
389 move |window, cx| {
390 context_picker.update(cx, |this, cx| {
391 this.add_recent_file(project_path.clone(), window, cx);
392 })
393 },
394 )
395 }
396 RecentEntry::Thread(thread) => {
397 let context_store = self.context_store.clone();
398 let view_thread = thread.clone();
399
400 ContextMenuItem::custom_entry(
401 move |_window, cx| {
402 render_thread_context_entry(&view_thread, context_store.clone(), cx)
403 .into_any()
404 },
405 move |_window, cx| {
406 context_picker.update(cx, |this, cx| {
407 this.add_recent_thread(thread.clone(), cx)
408 .detach_and_log_err(cx);
409 })
410 },
411 )
412 }
413 }
414 }
415
416 fn add_recent_file(
417 &self,
418 project_path: ProjectPath,
419 window: &mut Window,
420 cx: &mut Context<Self>,
421 ) {
422 let Some(context_store) = self.context_store.upgrade() else {
423 return;
424 };
425
426 let task = context_store.update(cx, |context_store, cx| {
427 context_store.add_file_from_path(project_path.clone(), true, cx)
428 });
429
430 cx.spawn_in(window, async move |_, cx| task.await.notify_async_err(cx))
431 .detach();
432
433 cx.notify();
434 }
435
436 fn add_recent_thread(
437 &self,
438 thread: ThreadContextEntry,
439 cx: &mut Context<Self>,
440 ) -> Task<Result<()>> {
441 let Some(context_store) = self.context_store.upgrade() else {
442 return Task::ready(Err(anyhow!("context store not available")));
443 };
444
445 let Some(thread_store) = self
446 .thread_store
447 .as_ref()
448 .and_then(|thread_store| thread_store.upgrade())
449 else {
450 return Task::ready(Err(anyhow!("thread store not available")));
451 };
452
453 let open_thread_task = thread_store.update(cx, |this, cx| this.open_thread(&thread.id, cx));
454 cx.spawn(async move |this, cx| {
455 let thread = open_thread_task.await?;
456 context_store.update(cx, |context_store, cx| {
457 context_store.add_thread(thread, true, cx);
458 })?;
459
460 this.update(cx, |_this, cx| cx.notify())
461 })
462 }
463
464 fn recent_entries(&self, cx: &mut App) -> Vec<RecentEntry> {
465 let Some(workspace) = self.workspace.upgrade() else {
466 return vec![];
467 };
468
469 let Some(context_store) = self.context_store.upgrade() else {
470 return vec![];
471 };
472
473 recent_context_picker_entries(context_store, self.thread_store.clone(), workspace, cx)
474 }
475
476 fn notify_current_picker(&mut self, cx: &mut Context<Self>) {
477 match &self.mode {
478 ContextPickerState::Default(entity) => entity.update(cx, |_, cx| cx.notify()),
479 ContextPickerState::File(entity) => entity.update(cx, |_, cx| cx.notify()),
480 ContextPickerState::Symbol(entity) => entity.update(cx, |_, cx| cx.notify()),
481 ContextPickerState::Fetch(entity) => entity.update(cx, |_, cx| cx.notify()),
482 ContextPickerState::Thread(entity) => entity.update(cx, |_, cx| cx.notify()),
483 ContextPickerState::Rules(entity) => entity.update(cx, |_, cx| cx.notify()),
484 }
485 }
486}
487
488impl EventEmitter<DismissEvent> for ContextPicker {}
489
490impl Focusable for ContextPicker {
491 fn focus_handle(&self, cx: &App) -> FocusHandle {
492 match &self.mode {
493 ContextPickerState::Default(menu) => menu.focus_handle(cx),
494 ContextPickerState::File(file_picker) => file_picker.focus_handle(cx),
495 ContextPickerState::Symbol(symbol_picker) => symbol_picker.focus_handle(cx),
496 ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
497 ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx),
498 ContextPickerState::Rules(user_rules_picker) => user_rules_picker.focus_handle(cx),
499 }
500 }
501}
502
503impl Render for ContextPicker {
504 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
505 v_flex()
506 .w(px(400.))
507 .min_w(px(400.))
508 .map(|parent| match &self.mode {
509 ContextPickerState::Default(menu) => parent.child(menu.clone()),
510 ContextPickerState::File(file_picker) => parent.child(file_picker.clone()),
511 ContextPickerState::Symbol(symbol_picker) => parent.child(symbol_picker.clone()),
512 ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
513 ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()),
514 ContextPickerState::Rules(user_rules_picker) => {
515 parent.child(user_rules_picker.clone())
516 }
517 })
518 }
519}
520enum RecentEntry {
521 File {
522 project_path: ProjectPath,
523 path_prefix: Arc<str>,
524 },
525 Thread(ThreadContextEntry),
526}
527
528fn available_context_picker_entries(
529 thread_store: &Option<WeakEntity<ThreadStore>>,
530 workspace: &Entity<Workspace>,
531 cx: &mut App,
532) -> Vec<ContextPickerEntry> {
533 let mut entries = vec![
534 ContextPickerEntry::Mode(ContextPickerMode::File),
535 ContextPickerEntry::Mode(ContextPickerMode::Symbol),
536 ];
537
538 let has_selection = workspace
539 .read(cx)
540 .active_item(cx)
541 .and_then(|item| item.downcast::<Editor>())
542 .map_or(false, |editor| {
543 editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))
544 });
545 if has_selection {
546 entries.push(ContextPickerEntry::Action(
547 ContextPickerAction::AddSelections,
548 ));
549 }
550
551 if thread_store.is_some() {
552 entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
553 entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
554 }
555
556 entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
557
558 entries
559}
560
561fn recent_context_picker_entries(
562 context_store: Entity<ContextStore>,
563 thread_store: Option<WeakEntity<ThreadStore>>,
564 workspace: Entity<Workspace>,
565 cx: &App,
566) -> Vec<RecentEntry> {
567 let mut recent = Vec::with_capacity(6);
568
569 let current_files = context_store.read(cx).file_paths(cx);
570 let workspace = workspace.read(cx);
571 let project = workspace.project().read(cx);
572
573 recent.extend(
574 workspace
575 .recent_navigation_history_iter(cx)
576 .filter(|(path, _)| !current_files.contains(path))
577 .take(4)
578 .filter_map(|(project_path, _)| {
579 project
580 .worktree_for_id(project_path.worktree_id, cx)
581 .map(|worktree| RecentEntry::File {
582 project_path,
583 path_prefix: worktree.read(cx).root_name().into(),
584 })
585 }),
586 );
587
588 let mut current_threads = context_store.read(cx).thread_ids();
589
590 if let Some(active_thread) = workspace
591 .panel::<AssistantPanel>(cx)
592 .map(|panel| panel.read(cx).active_thread(cx))
593 {
594 current_threads.insert(active_thread.read(cx).id().clone());
595 }
596
597 if let Some(thread_store) = thread_store.and_then(|thread_store| thread_store.upgrade()) {
598 recent.extend(
599 thread_store
600 .read(cx)
601 .threads()
602 .into_iter()
603 .filter(|thread| !current_threads.contains(&thread.id))
604 .take(2)
605 .map(|thread| {
606 RecentEntry::Thread(ThreadContextEntry {
607 id: thread.id,
608 summary: thread.summary,
609 })
610 }),
611 );
612 }
613
614 recent
615}
616
617fn add_selections_as_context(
618 context_store: &Entity<ContextStore>,
619 workspace: &Entity<Workspace>,
620 cx: &mut App,
621) {
622 let selection_ranges = selection_ranges(workspace, cx);
623 context_store.update(cx, |context_store, cx| {
624 for (buffer, range) in selection_ranges {
625 context_store
626 .add_selection(buffer, range, cx)
627 .detach_and_log_err(cx);
628 }
629 })
630}
631
632fn selection_ranges(
633 workspace: &Entity<Workspace>,
634 cx: &mut App,
635) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
636 let Some(editor) = workspace
637 .read(cx)
638 .active_item(cx)
639 .and_then(|item| item.act_as::<Editor>(cx))
640 else {
641 return Vec::new();
642 };
643
644 editor.update(cx, |editor, cx| {
645 let selections = editor.selections.all_adjusted(cx);
646
647 let buffer = editor.buffer().clone().read(cx);
648 let snapshot = buffer.snapshot(cx);
649
650 selections
651 .into_iter()
652 .map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
653 .flat_map(|range| {
654 let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
655 let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
656 if start_buffer != end_buffer {
657 return None;
658 }
659 Some((start_buffer, start..end))
660 })
661 .collect::<Vec<_>>()
662 })
663}
664
665pub(crate) fn insert_fold_for_mention(
666 excerpt_id: ExcerptId,
667 crease_start: text::Anchor,
668 content_len: usize,
669 crease_label: SharedString,
670 crease_icon_path: SharedString,
671 editor_entity: Entity<Editor>,
672 cx: &mut App,
673) {
674 editor_entity.update(cx, |editor, cx| {
675 let snapshot = editor.buffer().read(cx).snapshot(cx);
676
677 let Some(start) = snapshot.anchor_in_excerpt(excerpt_id, crease_start) else {
678 return;
679 };
680
681 let start = start.bias_right(&snapshot);
682 let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
683
684 let crease = crease_for_mention(
685 crease_label,
686 crease_icon_path,
687 start..end,
688 editor_entity.downgrade(),
689 );
690
691 editor.display_map.update(cx, |display_map, cx| {
692 display_map.fold(vec![crease], cx);
693 });
694 });
695}
696
697pub fn crease_for_mention(
698 label: SharedString,
699 icon_path: SharedString,
700 range: Range<Anchor>,
701 editor_entity: WeakEntity<Editor>,
702) -> Crease<Anchor> {
703 let placeholder = FoldPlaceholder {
704 render: render_fold_icon_button(icon_path, label, editor_entity),
705 merge_adjacent: false,
706 ..Default::default()
707 };
708
709 let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
710
711 let crease = Crease::inline(
712 range,
713 placeholder.clone(),
714 fold_toggle("mention"),
715 render_trailer,
716 );
717 crease
718}
719
720fn render_fold_icon_button(
721 icon_path: SharedString,
722 label: SharedString,
723 editor: WeakEntity<Editor>,
724) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
725 Arc::new({
726 move |fold_id, fold_range, cx| {
727 let is_in_text_selection = editor.upgrade().is_some_and(|editor| {
728 editor.update(cx, |editor, cx| {
729 let snapshot = editor
730 .buffer()
731 .update(cx, |multi_buffer, cx| multi_buffer.snapshot(cx));
732
733 let is_in_pending_selection = || {
734 editor
735 .selections
736 .pending
737 .as_ref()
738 .is_some_and(|pending_selection| {
739 pending_selection
740 .selection
741 .range()
742 .includes(&fold_range, &snapshot)
743 })
744 };
745
746 let mut is_in_complete_selection = || {
747 editor
748 .selections
749 .disjoint_in_range::<usize>(fold_range.clone(), cx)
750 .into_iter()
751 .any(|selection| {
752 // This is needed to cover a corner case, if we just check for an existing
753 // selection in the fold range, having a cursor at the start of the fold
754 // marks it as selected. Non-empty selections don't cause this.
755 let length = selection.end - selection.start;
756 length > 0
757 })
758 };
759
760 is_in_pending_selection() || is_in_complete_selection()
761 })
762 });
763
764 ButtonLike::new(fold_id)
765 .style(ButtonStyle::Filled)
766 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
767 .toggle_state(is_in_text_selection)
768 .child(
769 h_flex()
770 .gap_1()
771 .child(
772 Icon::from_path(icon_path.clone())
773 .size(IconSize::XSmall)
774 .color(Color::Muted),
775 )
776 .child(
777 Label::new(label.clone())
778 .size(LabelSize::Small)
779 .buffer_font(cx)
780 .single_line(),
781 ),
782 )
783 .into_any_element()
784 }
785 })
786}
787
788fn fold_toggle(
789 name: &'static str,
790) -> impl Fn(
791 MultiBufferRow,
792 bool,
793 Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
794 &mut Window,
795 &mut App,
796) -> AnyElement {
797 move |row, is_folded, fold, _window, _cx| {
798 Disclosure::new((name, row.0 as u64), !is_folded)
799 .toggle_state(is_folded)
800 .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
801 .into_any_element()
802 }
803}
804
805pub enum MentionLink {
806 File(ProjectPath, Entry),
807 Symbol(ProjectPath, String),
808 Selection(ProjectPath, Range<usize>),
809 Fetch(String),
810 Thread(ThreadId),
811 Rules(UserPromptId),
812}
813
814impl MentionLink {
815 const FILE: &str = "@file";
816 const SYMBOL: &str = "@symbol";
817 const SELECTION: &str = "@selection";
818 const THREAD: &str = "@thread";
819 const FETCH: &str = "@fetch";
820 const RULES: &str = "@rules";
821
822 const SEPARATOR: &str = ":";
823
824 pub fn is_valid(url: &str) -> bool {
825 url.starts_with(Self::FILE)
826 || url.starts_with(Self::SYMBOL)
827 || url.starts_with(Self::FETCH)
828 || url.starts_with(Self::SELECTION)
829 || url.starts_with(Self::THREAD)
830 || url.starts_with(Self::RULES)
831 }
832
833 pub fn for_file(file_name: &str, full_path: &str) -> String {
834 format!("[@{}]({}:{})", file_name, Self::FILE, full_path)
835 }
836
837 pub fn for_symbol(symbol_name: &str, full_path: &str) -> String {
838 format!(
839 "[@{}]({}:{}:{})",
840 symbol_name,
841 Self::SYMBOL,
842 full_path,
843 symbol_name
844 )
845 }
846
847 pub fn for_selection(file_name: &str, full_path: &str, line_range: Range<usize>) -> String {
848 format!(
849 "[@{} ({}-{})]({}:{}:{}-{})",
850 file_name,
851 line_range.start,
852 line_range.end,
853 Self::SELECTION,
854 full_path,
855 line_range.start,
856 line_range.end
857 )
858 }
859
860 pub fn for_thread(thread: &ThreadContextEntry) -> String {
861 format!("[@{}]({}:{})", thread.summary, Self::THREAD, thread.id)
862 }
863
864 pub fn for_fetch(url: &str) -> String {
865 format!("[@{}]({}:{})", url, Self::FETCH, url)
866 }
867
868 pub fn for_rules(rules: &RulesContextEntry) -> String {
869 format!("[@{}]({}:{})", rules.title, Self::RULES, rules.prompt_id.0)
870 }
871
872 pub fn try_parse(link: &str, workspace: &Entity<Workspace>, cx: &App) -> Option<Self> {
873 fn extract_project_path_from_link(
874 path: &str,
875 workspace: &Entity<Workspace>,
876 cx: &App,
877 ) -> Option<ProjectPath> {
878 let path = PathBuf::from(path);
879 let worktree_name = path.iter().next()?;
880 let path: PathBuf = path.iter().skip(1).collect();
881 let worktree_id = workspace
882 .read(cx)
883 .visible_worktrees(cx)
884 .find(|worktree| worktree.read(cx).root_name() == worktree_name)
885 .map(|worktree| worktree.read(cx).id())?;
886 Some(ProjectPath {
887 worktree_id,
888 path: path.into(),
889 })
890 }
891
892 let (prefix, argument) = link.split_once(Self::SEPARATOR)?;
893 match prefix {
894 Self::FILE => {
895 let project_path = extract_project_path_from_link(argument, workspace, cx)?;
896 let entry = workspace
897 .read(cx)
898 .project()
899 .read(cx)
900 .entry_for_path(&project_path, cx)?;
901 Some(MentionLink::File(project_path, entry))
902 }
903 Self::SYMBOL => {
904 let (path, symbol) = argument.split_once(Self::SEPARATOR)?;
905 let project_path = extract_project_path_from_link(path, workspace, cx)?;
906 Some(MentionLink::Symbol(project_path, symbol.to_string()))
907 }
908 Self::SELECTION => {
909 let (path, line_args) = argument.split_once(Self::SEPARATOR)?;
910 let project_path = extract_project_path_from_link(path, workspace, cx)?;
911
912 let line_range = {
913 let (start, end) = line_args
914 .trim_start_matches('(')
915 .trim_end_matches(')')
916 .split_once('-')?;
917 start.parse::<usize>().ok()?..end.parse::<usize>().ok()?
918 };
919
920 Some(MentionLink::Selection(project_path, line_range))
921 }
922 Self::THREAD => {
923 let thread_id = ThreadId::from(argument);
924 Some(MentionLink::Thread(thread_id))
925 }
926 Self::FETCH => Some(MentionLink::Fetch(argument.to_string())),
927 Self::RULES => {
928 let prompt_id = UserPromptId(Uuid::try_parse(argument).ok()?);
929 Some(MentionLink::Rules(prompt_id))
930 }
931 _ => None,
932 }
933 }
934}