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