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            "rule" => 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 => "rule",
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(
486            context_store,
487            self.thread_store.clone(),
488            workspace,
489            None,
490            cx,
491        )
492    }
493
494    fn notify_current_picker(&mut self, cx: &mut Context<Self>) {
495        match &self.mode {
496            ContextPickerState::Default(entity) => entity.update(cx, |_, cx| cx.notify()),
497            ContextPickerState::File(entity) => entity.update(cx, |_, cx| cx.notify()),
498            ContextPickerState::Symbol(entity) => entity.update(cx, |_, cx| cx.notify()),
499            ContextPickerState::Fetch(entity) => entity.update(cx, |_, cx| cx.notify()),
500            ContextPickerState::Thread(entity) => entity.update(cx, |_, cx| cx.notify()),
501            ContextPickerState::Rules(entity) => entity.update(cx, |_, cx| cx.notify()),
502        }
503    }
504}
505
506impl EventEmitter<DismissEvent> for ContextPicker {}
507
508impl Focusable for ContextPicker {
509    fn focus_handle(&self, cx: &App) -> FocusHandle {
510        match &self.mode {
511            ContextPickerState::Default(menu) => menu.focus_handle(cx),
512            ContextPickerState::File(file_picker) => file_picker.focus_handle(cx),
513            ContextPickerState::Symbol(symbol_picker) => symbol_picker.focus_handle(cx),
514            ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
515            ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx),
516            ContextPickerState::Rules(user_rules_picker) => user_rules_picker.focus_handle(cx),
517        }
518    }
519}
520
521impl Render for ContextPicker {
522    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
523        v_flex()
524            .w(px(400.))
525            .min_w(px(400.))
526            .map(|parent| match &self.mode {
527                ContextPickerState::Default(menu) => parent.child(menu.clone()),
528                ContextPickerState::File(file_picker) => parent.child(file_picker.clone()),
529                ContextPickerState::Symbol(symbol_picker) => parent.child(symbol_picker.clone()),
530                ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
531                ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()),
532                ContextPickerState::Rules(user_rules_picker) => {
533                    parent.child(user_rules_picker.clone())
534                }
535            })
536    }
537}
538enum RecentEntry {
539    File {
540        project_path: ProjectPath,
541        path_prefix: Arc<str>,
542    },
543    Thread(ThreadContextEntry),
544}
545
546fn available_context_picker_entries(
547    prompt_store: &Option<Entity<PromptStore>>,
548    thread_store: &Option<WeakEntity<ThreadStore>>,
549    workspace: &Entity<Workspace>,
550    cx: &mut App,
551) -> Vec<ContextPickerEntry> {
552    let mut entries = vec![
553        ContextPickerEntry::Mode(ContextPickerMode::File),
554        ContextPickerEntry::Mode(ContextPickerMode::Symbol),
555    ];
556
557    let has_selection = workspace
558        .read(cx)
559        .active_item(cx)
560        .and_then(|item| item.downcast::<Editor>())
561        .map_or(false, |editor| {
562            editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))
563        });
564    if has_selection {
565        entries.push(ContextPickerEntry::Action(
566            ContextPickerAction::AddSelections,
567        ));
568    }
569
570    if thread_store.is_some() {
571        entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
572    }
573
574    if prompt_store.is_some() {
575        entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
576    }
577
578    entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
579
580    entries
581}
582
583fn recent_context_picker_entries(
584    context_store: Entity<ContextStore>,
585    thread_store: Option<WeakEntity<ThreadStore>>,
586    workspace: Entity<Workspace>,
587    exclude_path: Option<ProjectPath>,
588    cx: &App,
589) -> Vec<RecentEntry> {
590    let mut recent = Vec::with_capacity(6);
591    let mut current_files = context_store.read(cx).file_paths(cx);
592    current_files.extend(exclude_path);
593    let workspace = workspace.read(cx);
594    let project = workspace.project().read(cx);
595
596    recent.extend(
597        workspace
598            .recent_navigation_history_iter(cx)
599            .filter(|(path, _)| !current_files.contains(path))
600            .take(4)
601            .filter_map(|(project_path, _)| {
602                project
603                    .worktree_for_id(project_path.worktree_id, cx)
604                    .map(|worktree| RecentEntry::File {
605                        project_path,
606                        path_prefix: worktree.read(cx).root_name().into(),
607                    })
608            }),
609    );
610
611    let current_threads = context_store.read(cx).thread_ids();
612
613    let active_thread_id = workspace
614        .panel::<AssistantPanel>(cx)
615        .map(|panel| panel.read(cx).active_thread(cx).read(cx).id());
616
617    if let Some(thread_store) = thread_store.and_then(|thread_store| thread_store.upgrade()) {
618        recent.extend(
619            thread_store
620                .read(cx)
621                .reverse_chronological_threads()
622                .into_iter()
623                .filter(|thread| {
624                    Some(&thread.id) != active_thread_id && !current_threads.contains(&thread.id)
625                })
626                .take(2)
627                .map(|thread| {
628                    RecentEntry::Thread(ThreadContextEntry {
629                        id: thread.id,
630                        summary: thread.summary,
631                    })
632                }),
633        );
634    }
635
636    recent
637}
638
639fn add_selections_as_context(
640    context_store: &Entity<ContextStore>,
641    workspace: &Entity<Workspace>,
642    cx: &mut App,
643) {
644    let selection_ranges = selection_ranges(workspace, cx);
645    context_store.update(cx, |context_store, cx| {
646        for (buffer, range) in selection_ranges {
647            context_store.add_selection(buffer, range, cx);
648        }
649    })
650}
651
652fn selection_ranges(
653    workspace: &Entity<Workspace>,
654    cx: &mut App,
655) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
656    let Some(editor) = workspace
657        .read(cx)
658        .active_item(cx)
659        .and_then(|item| item.act_as::<Editor>(cx))
660    else {
661        return Vec::new();
662    };
663
664    editor.update(cx, |editor, cx| {
665        let selections = editor.selections.all_adjusted(cx);
666
667        let buffer = editor.buffer().clone().read(cx);
668        let snapshot = buffer.snapshot(cx);
669
670        selections
671            .into_iter()
672            .map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
673            .flat_map(|range| {
674                let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
675                let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
676                if start_buffer != end_buffer {
677                    return None;
678                }
679                Some((start_buffer, start..end))
680            })
681            .collect::<Vec<_>>()
682    })
683}
684
685pub(crate) fn insert_crease_for_mention(
686    excerpt_id: ExcerptId,
687    crease_start: text::Anchor,
688    content_len: usize,
689    crease_label: SharedString,
690    crease_icon_path: SharedString,
691    editor_entity: Entity<Editor>,
692    window: &mut Window,
693    cx: &mut App,
694) -> Option<CreaseId> {
695    editor_entity.update(cx, |editor, cx| {
696        let snapshot = editor.buffer().read(cx).snapshot(cx);
697
698        let start = snapshot.anchor_in_excerpt(excerpt_id, crease_start)?;
699
700        let start = start.bias_right(&snapshot);
701        let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
702
703        let crease = crease_for_mention(
704            crease_label,
705            crease_icon_path,
706            start..end,
707            editor_entity.downgrade(),
708        );
709
710        let ids = editor.insert_creases(vec![crease.clone()], cx);
711        editor.fold_creases(vec![crease], false, window, cx);
712        Some(ids[0])
713    })
714}
715
716pub fn crease_for_mention(
717    label: SharedString,
718    icon_path: SharedString,
719    range: Range<Anchor>,
720    editor_entity: WeakEntity<Editor>,
721) -> Crease<Anchor> {
722    let placeholder = FoldPlaceholder {
723        render: render_fold_icon_button(icon_path.clone(), label.clone(), editor_entity),
724        merge_adjacent: false,
725        ..Default::default()
726    };
727
728    let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
729
730    Crease::inline(
731        range,
732        placeholder.clone(),
733        fold_toggle("mention"),
734        render_trailer,
735    )
736    .with_metadata(CreaseMetadata { icon_path, label })
737}
738
739fn render_fold_icon_button(
740    icon_path: SharedString,
741    label: SharedString,
742    editor: WeakEntity<Editor>,
743) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
744    Arc::new({
745        move |fold_id, fold_range, cx| {
746            let is_in_text_selection = editor.upgrade().is_some_and(|editor| {
747                editor.update(cx, |editor, cx| {
748                    let snapshot = editor
749                        .buffer()
750                        .update(cx, |multi_buffer, cx| multi_buffer.snapshot(cx));
751
752                    let is_in_pending_selection = || {
753                        editor
754                            .selections
755                            .pending
756                            .as_ref()
757                            .is_some_and(|pending_selection| {
758                                pending_selection
759                                    .selection
760                                    .range()
761                                    .includes(&fold_range, &snapshot)
762                            })
763                    };
764
765                    let mut is_in_complete_selection = || {
766                        editor
767                            .selections
768                            .disjoint_in_range::<usize>(fold_range.clone(), cx)
769                            .into_iter()
770                            .any(|selection| {
771                                // This is needed to cover a corner case, if we just check for an existing
772                                // selection in the fold range, having a cursor at the start of the fold
773                                // marks it as selected. Non-empty selections don't cause this.
774                                let length = selection.end - selection.start;
775                                length > 0
776                            })
777                    };
778
779                    is_in_pending_selection() || is_in_complete_selection()
780                })
781            });
782
783            ButtonLike::new(fold_id)
784                .style(ButtonStyle::Filled)
785                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
786                .toggle_state(is_in_text_selection)
787                .child(
788                    h_flex()
789                        .gap_1()
790                        .child(
791                            Icon::from_path(icon_path.clone())
792                                .size(IconSize::XSmall)
793                                .color(Color::Muted),
794                        )
795                        .child(
796                            Label::new(label.clone())
797                                .size(LabelSize::Small)
798                                .buffer_font(cx)
799                                .single_line(),
800                        ),
801                )
802                .into_any_element()
803        }
804    })
805}
806
807fn fold_toggle(
808    name: &'static str,
809) -> impl Fn(
810    MultiBufferRow,
811    bool,
812    Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
813    &mut Window,
814    &mut App,
815) -> AnyElement {
816    move |row, is_folded, fold, _window, _cx| {
817        Disclosure::new((name, row.0 as u64), !is_folded)
818            .toggle_state(is_folded)
819            .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
820            .into_any_element()
821    }
822}
823
824pub enum MentionLink {
825    File(ProjectPath, Entry),
826    Symbol(ProjectPath, String),
827    Selection(ProjectPath, Range<usize>),
828    Fetch(String),
829    Thread(ThreadId),
830    Rule(UserPromptId),
831}
832
833impl MentionLink {
834    const FILE: &str = "@file";
835    const SYMBOL: &str = "@symbol";
836    const SELECTION: &str = "@selection";
837    const THREAD: &str = "@thread";
838    const FETCH: &str = "@fetch";
839    const RULE: &str = "@rule";
840
841    const SEPARATOR: &str = ":";
842
843    pub fn is_valid(url: &str) -> bool {
844        url.starts_with(Self::FILE)
845            || url.starts_with(Self::SYMBOL)
846            || url.starts_with(Self::FETCH)
847            || url.starts_with(Self::SELECTION)
848            || url.starts_with(Self::THREAD)
849            || url.starts_with(Self::RULE)
850    }
851
852    pub fn for_file(file_name: &str, full_path: &str) -> String {
853        format!("[@{}]({}:{})", file_name, Self::FILE, full_path)
854    }
855
856    pub fn for_symbol(symbol_name: &str, full_path: &str) -> String {
857        format!(
858            "[@{}]({}:{}:{})",
859            symbol_name,
860            Self::SYMBOL,
861            full_path,
862            symbol_name
863        )
864    }
865
866    pub fn for_selection(file_name: &str, full_path: &str, line_range: Range<usize>) -> String {
867        format!(
868            "[@{} ({}-{})]({}:{}:{}-{})",
869            file_name,
870            line_range.start,
871            line_range.end,
872            Self::SELECTION,
873            full_path,
874            line_range.start,
875            line_range.end
876        )
877    }
878
879    pub fn for_thread(thread: &ThreadContextEntry) -> String {
880        format!("[@{}]({}:{})", thread.summary, Self::THREAD, thread.id)
881    }
882
883    pub fn for_fetch(url: &str) -> String {
884        format!("[@{}]({}:{})", url, Self::FETCH, url)
885    }
886
887    pub fn for_rule(rule: &RulesContextEntry) -> String {
888        format!("[@{}]({}:{})", rule.title, Self::RULE, rule.prompt_id.0)
889    }
890
891    pub fn try_parse(link: &str, workspace: &Entity<Workspace>, cx: &App) -> Option<Self> {
892        fn extract_project_path_from_link(
893            path: &str,
894            workspace: &Entity<Workspace>,
895            cx: &App,
896        ) -> Option<ProjectPath> {
897            let path = PathBuf::from(path);
898            let worktree_name = path.iter().next()?;
899            let path: PathBuf = path.iter().skip(1).collect();
900            let worktree_id = workspace
901                .read(cx)
902                .visible_worktrees(cx)
903                .find(|worktree| worktree.read(cx).root_name() == worktree_name)
904                .map(|worktree| worktree.read(cx).id())?;
905            Some(ProjectPath {
906                worktree_id,
907                path: path.into(),
908            })
909        }
910
911        let (prefix, argument) = link.split_once(Self::SEPARATOR)?;
912        match prefix {
913            Self::FILE => {
914                let project_path = extract_project_path_from_link(argument, workspace, cx)?;
915                let entry = workspace
916                    .read(cx)
917                    .project()
918                    .read(cx)
919                    .entry_for_path(&project_path, cx)?;
920                Some(MentionLink::File(project_path, entry))
921            }
922            Self::SYMBOL => {
923                let (path, symbol) = argument.split_once(Self::SEPARATOR)?;
924                let project_path = extract_project_path_from_link(path, workspace, cx)?;
925                Some(MentionLink::Symbol(project_path, symbol.to_string()))
926            }
927            Self::SELECTION => {
928                let (path, line_args) = argument.split_once(Self::SEPARATOR)?;
929                let project_path = extract_project_path_from_link(path, workspace, cx)?;
930
931                let line_range = {
932                    let (start, end) = line_args
933                        .trim_start_matches('(')
934                        .trim_end_matches(')')
935                        .split_once('-')?;
936                    start.parse::<usize>().ok()?..end.parse::<usize>().ok()?
937                };
938
939                Some(MentionLink::Selection(project_path, line_range))
940            }
941            Self::THREAD => {
942                let thread_id = ThreadId::from(argument);
943                Some(MentionLink::Thread(thread_id))
944            }
945            Self::FETCH => Some(MentionLink::Fetch(argument.to_string())),
946            Self::RULE => {
947                let prompt_id = UserPromptId(Uuid::try_parse(argument).ok()?);
948                Some(MentionLink::Rule(prompt_id))
949            }
950            _ => None,
951        }
952    }
953}