context_picker.rs

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