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