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