context_picker.rs

  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}