context_picker.rs

   1mod completion_provider;
   2mod fetch_context_picker;
   3mod file_context_picker;
   4mod rules_context_picker;
   5mod symbol_context_picker;
   6mod thread_context_picker;
   7
   8use std::ops::Range;
   9use std::path::{Path, PathBuf};
  10use std::sync::Arc;
  11
  12use anyhow::{Result, anyhow};
  13pub use completion_provider::ContextPickerCompletionProvider;
  14use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId};
  15use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
  16use fetch_context_picker::FetchContextPicker;
  17use file_context_picker::FileContextPicker;
  18use file_context_picker::render_file_context_entry;
  19use gpui::{
  20    App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
  21    WeakEntity,
  22};
  23use language::Buffer;
  24use multi_buffer::MultiBufferRow;
  25use paths::contexts_dir;
  26use project::{Entry, ProjectPath};
  27use prompt_store::{PromptStore, UserPromptId};
  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 uuid::Uuid;
  37use workspace::{Workspace, notifications::NotifyResultExt};
  38
  39use crate::AssistantPanel;
  40use crate::context::RULES_ICON;
  41use crate::context_store::ContextStore;
  42use crate::thread::ThreadId;
  43use crate::thread_store::{TextThreadStore, ThreadStore};
  44
  45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
  46enum 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)]
  75enum ContextPickerMode {
  76    File,
  77    Symbol,
  78    Fetch,
  79    Thread,
  80    Rules,
  81}
  82
  83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
  84enum 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::Context,
 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::Globe,
 149            Self::Thread => IconName::MessageBubbles,
 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().clone();
 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    fn recent_menu_item(
 385        &self,
 386        context_picker: Entity<ContextPicker>,
 387        ix: usize,
 388        entry: RecentEntry,
 389    ) -> ContextMenuItem {
 390        match entry {
 391            RecentEntry::File {
 392                project_path,
 393                path_prefix,
 394            } => {
 395                let context_store = self.context_store.clone();
 396                let worktree_id = project_path.worktree_id;
 397                let path = project_path.path.clone();
 398
 399                ContextMenuItem::custom_entry(
 400                    move |_window, cx| {
 401                        render_file_context_entry(
 402                            ElementId::named_usize("ctx-recent", ix),
 403                            worktree_id,
 404                            &path,
 405                            &path_prefix,
 406                            false,
 407                            context_store.clone(),
 408                            cx,
 409                        )
 410                        .into_any()
 411                    },
 412                    move |window, cx| {
 413                        context_picker.update(cx, |this, cx| {
 414                            this.add_recent_file(project_path.clone(), window, cx);
 415                        })
 416                    },
 417                )
 418            }
 419            RecentEntry::Thread(thread) => {
 420                let context_store = self.context_store.clone();
 421                let view_thread = thread.clone();
 422
 423                ContextMenuItem::custom_entry(
 424                    move |_window, cx| {
 425                        render_thread_context_entry(&view_thread, context_store.clone(), cx)
 426                            .into_any()
 427                    },
 428                    move |_window, cx| {
 429                        context_picker.update(cx, |this, cx| {
 430                            this.add_recent_thread(thread.clone(), cx)
 431                                .detach_and_log_err(cx);
 432                        })
 433                    },
 434                )
 435            }
 436        }
 437    }
 438
 439    fn add_recent_file(
 440        &self,
 441        project_path: ProjectPath,
 442        window: &mut Window,
 443        cx: &mut Context<Self>,
 444    ) {
 445        let Some(context_store) = self.context_store.upgrade() else {
 446            return;
 447        };
 448
 449        let task = context_store.update(cx, |context_store, cx| {
 450            context_store.add_file_from_path(project_path.clone(), true, cx)
 451        });
 452
 453        cx.spawn_in(window, async move |_, cx| task.await.notify_async_err(cx))
 454            .detach();
 455
 456        cx.notify();
 457    }
 458
 459    fn add_recent_thread(
 460        &self,
 461        entry: ThreadContextEntry,
 462        cx: &mut Context<Self>,
 463    ) -> Task<Result<()>> {
 464        let Some(context_store) = self.context_store.upgrade() else {
 465            return Task::ready(Err(anyhow!("context store not available")));
 466        };
 467
 468        match entry {
 469            ThreadContextEntry::Thread { id, .. } => {
 470                let Some(thread_store) = self
 471                    .thread_store
 472                    .as_ref()
 473                    .and_then(|thread_store| thread_store.upgrade())
 474                else {
 475                    return Task::ready(Err(anyhow!("thread store not available")));
 476                };
 477
 478                let open_thread_task =
 479                    thread_store.update(cx, |this, cx| this.open_thread(&id, cx));
 480                cx.spawn(async move |this, cx| {
 481                    let thread = open_thread_task.await?;
 482                    context_store.update(cx, |context_store, cx| {
 483                        context_store.add_thread(thread, true, cx);
 484                    })?;
 485                    this.update(cx, |_this, cx| cx.notify())
 486                })
 487            }
 488            ThreadContextEntry::Context { path, .. } => {
 489                let Some(text_thread_store) = self
 490                    .text_thread_store
 491                    .as_ref()
 492                    .and_then(|thread_store| thread_store.upgrade())
 493                else {
 494                    return Task::ready(Err(anyhow!("text thread store not available")));
 495                };
 496
 497                let task = text_thread_store
 498                    .update(cx, |this, cx| this.open_local_context(path.clone(), cx));
 499                cx.spawn(async move |this, cx| {
 500                    let thread = task.await?;
 501                    context_store.update(cx, |context_store, cx| {
 502                        context_store.add_text_thread(thread, true, cx);
 503                    })?;
 504                    this.update(cx, |_this, cx| cx.notify())
 505                })
 506            }
 507        }
 508    }
 509
 510    fn recent_entries(&self, cx: &mut App) -> Vec<RecentEntry> {
 511        let Some(workspace) = self.workspace.upgrade() else {
 512            return vec![];
 513        };
 514
 515        let Some(context_store) = self.context_store.upgrade() else {
 516            return vec![];
 517        };
 518
 519        recent_context_picker_entries(
 520            context_store,
 521            self.thread_store.clone(),
 522            self.text_thread_store.clone(),
 523            workspace,
 524            None,
 525            cx,
 526        )
 527    }
 528
 529    fn notify_current_picker(&mut self, cx: &mut Context<Self>) {
 530        match &self.mode {
 531            ContextPickerState::Default(entity) => entity.update(cx, |_, cx| cx.notify()),
 532            ContextPickerState::File(entity) => entity.update(cx, |_, cx| cx.notify()),
 533            ContextPickerState::Symbol(entity) => entity.update(cx, |_, cx| cx.notify()),
 534            ContextPickerState::Fetch(entity) => entity.update(cx, |_, cx| cx.notify()),
 535            ContextPickerState::Thread(entity) => entity.update(cx, |_, cx| cx.notify()),
 536            ContextPickerState::Rules(entity) => entity.update(cx, |_, cx| cx.notify()),
 537        }
 538    }
 539}
 540
 541impl EventEmitter<DismissEvent> for ContextPicker {}
 542
 543impl Focusable for ContextPicker {
 544    fn focus_handle(&self, cx: &App) -> FocusHandle {
 545        match &self.mode {
 546            ContextPickerState::Default(menu) => menu.focus_handle(cx),
 547            ContextPickerState::File(file_picker) => file_picker.focus_handle(cx),
 548            ContextPickerState::Symbol(symbol_picker) => symbol_picker.focus_handle(cx),
 549            ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
 550            ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx),
 551            ContextPickerState::Rules(user_rules_picker) => user_rules_picker.focus_handle(cx),
 552        }
 553    }
 554}
 555
 556impl Render for ContextPicker {
 557    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
 558        v_flex()
 559            .w(px(400.))
 560            .min_w(px(400.))
 561            .map(|parent| match &self.mode {
 562                ContextPickerState::Default(menu) => parent.child(menu.clone()),
 563                ContextPickerState::File(file_picker) => parent.child(file_picker.clone()),
 564                ContextPickerState::Symbol(symbol_picker) => parent.child(symbol_picker.clone()),
 565                ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
 566                ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()),
 567                ContextPickerState::Rules(user_rules_picker) => {
 568                    parent.child(user_rules_picker.clone())
 569                }
 570            })
 571    }
 572}
 573enum RecentEntry {
 574    File {
 575        project_path: ProjectPath,
 576        path_prefix: Arc<str>,
 577    },
 578    Thread(ThreadContextEntry),
 579}
 580
 581fn available_context_picker_entries(
 582    prompt_store: &Option<Entity<PromptStore>>,
 583    thread_store: &Option<WeakEntity<ThreadStore>>,
 584    workspace: &Entity<Workspace>,
 585    cx: &mut App,
 586) -> Vec<ContextPickerEntry> {
 587    let mut entries = vec![
 588        ContextPickerEntry::Mode(ContextPickerMode::File),
 589        ContextPickerEntry::Mode(ContextPickerMode::Symbol),
 590    ];
 591
 592    let has_selection = workspace
 593        .read(cx)
 594        .active_item(cx)
 595        .and_then(|item| item.downcast::<Editor>())
 596        .map_or(false, |editor| {
 597            editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))
 598        });
 599    if has_selection {
 600        entries.push(ContextPickerEntry::Action(
 601            ContextPickerAction::AddSelections,
 602        ));
 603    }
 604
 605    if thread_store.is_some() {
 606        entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
 607    }
 608
 609    if prompt_store.is_some() {
 610        entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
 611    }
 612
 613    entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
 614
 615    entries
 616}
 617
 618fn recent_context_picker_entries(
 619    context_store: Entity<ContextStore>,
 620    thread_store: Option<WeakEntity<ThreadStore>>,
 621    text_thread_store: Option<WeakEntity<TextThreadStore>>,
 622    workspace: Entity<Workspace>,
 623    exclude_path: Option<ProjectPath>,
 624    cx: &App,
 625) -> Vec<RecentEntry> {
 626    let mut recent = Vec::with_capacity(6);
 627    let mut current_files = context_store.read(cx).file_paths(cx);
 628    current_files.extend(exclude_path);
 629    let workspace = workspace.read(cx);
 630    let project = workspace.project().read(cx);
 631
 632    recent.extend(
 633        workspace
 634            .recent_navigation_history_iter(cx)
 635            .filter(|(path, _)| !current_files.contains(path))
 636            .take(4)
 637            .filter_map(|(project_path, _)| {
 638                project
 639                    .worktree_for_id(project_path.worktree_id, cx)
 640                    .map(|worktree| RecentEntry::File {
 641                        project_path,
 642                        path_prefix: worktree.read(cx).root_name().into(),
 643                    })
 644            }),
 645    );
 646
 647    let current_threads = context_store.read(cx).thread_ids();
 648
 649    let active_thread_id = workspace
 650        .panel::<AssistantPanel>(cx)
 651        .and_then(|panel| Some(panel.read(cx).active_thread()?.read(cx).id()));
 652
 653    if let Some((thread_store, text_thread_store)) = thread_store
 654        .and_then(|store| store.upgrade())
 655        .zip(text_thread_store.and_then(|store| store.upgrade()))
 656    {
 657        let mut threads = unordered_thread_entries(thread_store, text_thread_store, cx)
 658            .filter(|(_, thread)| match thread {
 659                ThreadContextEntry::Thread { id, .. } => {
 660                    Some(id) != active_thread_id && !current_threads.contains(id)
 661                }
 662                ThreadContextEntry::Context { .. } => true,
 663            })
 664            .collect::<Vec<_>>();
 665
 666        const RECENT_COUNT: usize = 2;
 667        if threads.len() > RECENT_COUNT {
 668            threads.select_nth_unstable_by_key(RECENT_COUNT - 1, |(updated_at, _)| {
 669                std::cmp::Reverse(*updated_at)
 670            });
 671            threads.truncate(RECENT_COUNT);
 672        }
 673        threads.sort_unstable_by_key(|(updated_at, _)| std::cmp::Reverse(*updated_at));
 674
 675        recent.extend(
 676            threads
 677                .into_iter()
 678                .map(|(_, thread)| RecentEntry::Thread(thread)),
 679        );
 680    }
 681
 682    recent
 683}
 684
 685fn add_selections_as_context(
 686    context_store: &Entity<ContextStore>,
 687    workspace: &Entity<Workspace>,
 688    cx: &mut App,
 689) {
 690    let selection_ranges = selection_ranges(workspace, cx);
 691    context_store.update(cx, |context_store, cx| {
 692        for (buffer, range) in selection_ranges {
 693            context_store.add_selection(buffer, range, cx);
 694        }
 695    })
 696}
 697
 698fn selection_ranges(
 699    workspace: &Entity<Workspace>,
 700    cx: &mut App,
 701) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
 702    let Some(editor) = workspace
 703        .read(cx)
 704        .active_item(cx)
 705        .and_then(|item| item.act_as::<Editor>(cx))
 706    else {
 707        return Vec::new();
 708    };
 709
 710    editor.update(cx, |editor, cx| {
 711        let selections = editor.selections.all_adjusted(cx);
 712
 713        let buffer = editor.buffer().clone().read(cx);
 714        let snapshot = buffer.snapshot(cx);
 715
 716        selections
 717            .into_iter()
 718            .map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
 719            .flat_map(|range| {
 720                let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
 721                let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
 722                if start_buffer != end_buffer {
 723                    return None;
 724                }
 725                Some((start_buffer, start..end))
 726            })
 727            .collect::<Vec<_>>()
 728    })
 729}
 730
 731pub(crate) fn insert_crease_for_mention(
 732    excerpt_id: ExcerptId,
 733    crease_start: text::Anchor,
 734    content_len: usize,
 735    crease_label: SharedString,
 736    crease_icon_path: SharedString,
 737    editor_entity: Entity<Editor>,
 738    window: &mut Window,
 739    cx: &mut App,
 740) -> Option<CreaseId> {
 741    editor_entity.update(cx, |editor, cx| {
 742        let snapshot = editor.buffer().read(cx).snapshot(cx);
 743
 744        let start = snapshot.anchor_in_excerpt(excerpt_id, crease_start)?;
 745
 746        let start = start.bias_right(&snapshot);
 747        let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
 748
 749        let crease = crease_for_mention(
 750            crease_label,
 751            crease_icon_path,
 752            start..end,
 753            editor_entity.downgrade(),
 754        );
 755
 756        let ids = editor.insert_creases(vec![crease.clone()], cx);
 757        editor.fold_creases(vec![crease], false, window, cx);
 758        Some(ids[0])
 759    })
 760}
 761
 762pub fn crease_for_mention(
 763    label: SharedString,
 764    icon_path: SharedString,
 765    range: Range<Anchor>,
 766    editor_entity: WeakEntity<Editor>,
 767) -> Crease<Anchor> {
 768    let placeholder = FoldPlaceholder {
 769        render: render_fold_icon_button(icon_path.clone(), label.clone(), editor_entity),
 770        merge_adjacent: false,
 771        ..Default::default()
 772    };
 773
 774    let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
 775
 776    Crease::inline(
 777        range,
 778        placeholder.clone(),
 779        fold_toggle("mention"),
 780        render_trailer,
 781    )
 782    .with_metadata(CreaseMetadata { icon_path, label })
 783}
 784
 785fn render_fold_icon_button(
 786    icon_path: SharedString,
 787    label: SharedString,
 788    editor: WeakEntity<Editor>,
 789) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
 790    Arc::new({
 791        move |fold_id, fold_range, cx| {
 792            let is_in_text_selection = editor.upgrade().is_some_and(|editor| {
 793                editor.update(cx, |editor, cx| {
 794                    let snapshot = editor
 795                        .buffer()
 796                        .update(cx, |multi_buffer, cx| multi_buffer.snapshot(cx));
 797
 798                    let is_in_pending_selection = || {
 799                        editor
 800                            .selections
 801                            .pending
 802                            .as_ref()
 803                            .is_some_and(|pending_selection| {
 804                                pending_selection
 805                                    .selection
 806                                    .range()
 807                                    .includes(&fold_range, &snapshot)
 808                            })
 809                    };
 810
 811                    let mut is_in_complete_selection = || {
 812                        editor
 813                            .selections
 814                            .disjoint_in_range::<usize>(fold_range.clone(), cx)
 815                            .into_iter()
 816                            .any(|selection| {
 817                                // This is needed to cover a corner case, if we just check for an existing
 818                                // selection in the fold range, having a cursor at the start of the fold
 819                                // marks it as selected. Non-empty selections don't cause this.
 820                                let length = selection.end - selection.start;
 821                                length > 0
 822                            })
 823                    };
 824
 825                    is_in_pending_selection() || is_in_complete_selection()
 826                })
 827            });
 828
 829            ButtonLike::new(fold_id)
 830                .style(ButtonStyle::Filled)
 831                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
 832                .toggle_state(is_in_text_selection)
 833                .child(
 834                    h_flex()
 835                        .gap_1()
 836                        .child(
 837                            Icon::from_path(icon_path.clone())
 838                                .size(IconSize::XSmall)
 839                                .color(Color::Muted),
 840                        )
 841                        .child(
 842                            Label::new(label.clone())
 843                                .size(LabelSize::Small)
 844                                .buffer_font(cx)
 845                                .single_line(),
 846                        ),
 847                )
 848                .into_any_element()
 849        }
 850    })
 851}
 852
 853fn fold_toggle(
 854    name: &'static str,
 855) -> impl Fn(
 856    MultiBufferRow,
 857    bool,
 858    Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
 859    &mut Window,
 860    &mut App,
 861) -> AnyElement {
 862    move |row, is_folded, fold, _window, _cx| {
 863        Disclosure::new((name, row.0 as u64), !is_folded)
 864            .toggle_state(is_folded)
 865            .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
 866            .into_any_element()
 867    }
 868}
 869
 870pub enum MentionLink {
 871    File(ProjectPath, Entry),
 872    Symbol(ProjectPath, String),
 873    Selection(ProjectPath, Range<usize>),
 874    Fetch(String),
 875    Thread(ThreadId),
 876    TextThread(Arc<Path>),
 877    Rule(UserPromptId),
 878}
 879
 880impl MentionLink {
 881    const FILE: &str = "@file";
 882    const SYMBOL: &str = "@symbol";
 883    const SELECTION: &str = "@selection";
 884    const THREAD: &str = "@thread";
 885    const FETCH: &str = "@fetch";
 886    const RULE: &str = "@rule";
 887
 888    const TEXT_THREAD_URL_PREFIX: &str = "text-thread://";
 889
 890    const SEPARATOR: &str = ":";
 891
 892    pub fn is_valid(url: &str) -> bool {
 893        url.starts_with(Self::FILE)
 894            || url.starts_with(Self::SYMBOL)
 895            || url.starts_with(Self::FETCH)
 896            || url.starts_with(Self::SELECTION)
 897            || url.starts_with(Self::THREAD)
 898            || url.starts_with(Self::RULE)
 899    }
 900
 901    pub fn for_file(file_name: &str, full_path: &str) -> String {
 902        format!("[@{}]({}:{})", file_name, Self::FILE, full_path)
 903    }
 904
 905    pub fn for_symbol(symbol_name: &str, full_path: &str) -> String {
 906        format!(
 907            "[@{}]({}:{}:{})",
 908            symbol_name,
 909            Self::SYMBOL,
 910            full_path,
 911            symbol_name
 912        )
 913    }
 914
 915    pub fn for_selection(file_name: &str, full_path: &str, line_range: Range<usize>) -> String {
 916        format!(
 917            "[@{} ({}-{})]({}:{}:{}-{})",
 918            file_name,
 919            line_range.start,
 920            line_range.end,
 921            Self::SELECTION,
 922            full_path,
 923            line_range.start,
 924            line_range.end
 925        )
 926    }
 927
 928    pub fn for_thread(thread: &ThreadContextEntry) -> String {
 929        match thread {
 930            ThreadContextEntry::Thread { id, title } => {
 931                format!("[@{}]({}:{})", title, Self::THREAD, id)
 932            }
 933            ThreadContextEntry::Context { path, title } => {
 934                let filename = path.file_name().unwrap_or_default();
 935                let escaped_filename = urlencoding::encode(&filename.to_string_lossy()).to_string();
 936                format!(
 937                    "[@{}]({}:{}{})",
 938                    title,
 939                    Self::THREAD,
 940                    Self::TEXT_THREAD_URL_PREFIX,
 941                    escaped_filename
 942                )
 943            }
 944        }
 945    }
 946
 947    pub fn for_fetch(url: &str) -> String {
 948        format!("[@{}]({}:{})", url, Self::FETCH, url)
 949    }
 950
 951    pub fn for_rule(rule: &RulesContextEntry) -> String {
 952        format!("[@{}]({}:{})", rule.title, Self::RULE, rule.prompt_id.0)
 953    }
 954
 955    pub fn try_parse(link: &str, workspace: &Entity<Workspace>, cx: &App) -> Option<Self> {
 956        fn extract_project_path_from_link(
 957            path: &str,
 958            workspace: &Entity<Workspace>,
 959            cx: &App,
 960        ) -> Option<ProjectPath> {
 961            let path = PathBuf::from(path);
 962            let worktree_name = path.iter().next()?;
 963            let path: PathBuf = path.iter().skip(1).collect();
 964            let worktree_id = workspace
 965                .read(cx)
 966                .visible_worktrees(cx)
 967                .find(|worktree| worktree.read(cx).root_name() == worktree_name)
 968                .map(|worktree| worktree.read(cx).id())?;
 969            Some(ProjectPath {
 970                worktree_id,
 971                path: path.into(),
 972            })
 973        }
 974
 975        let (prefix, argument) = link.split_once(Self::SEPARATOR)?;
 976        match prefix {
 977            Self::FILE => {
 978                let project_path = extract_project_path_from_link(argument, workspace, cx)?;
 979                let entry = workspace
 980                    .read(cx)
 981                    .project()
 982                    .read(cx)
 983                    .entry_for_path(&project_path, cx)?;
 984                Some(MentionLink::File(project_path, entry))
 985            }
 986            Self::SYMBOL => {
 987                let (path, symbol) = argument.split_once(Self::SEPARATOR)?;
 988                let project_path = extract_project_path_from_link(path, workspace, cx)?;
 989                Some(MentionLink::Symbol(project_path, symbol.to_string()))
 990            }
 991            Self::SELECTION => {
 992                let (path, line_args) = argument.split_once(Self::SEPARATOR)?;
 993                let project_path = extract_project_path_from_link(path, workspace, cx)?;
 994
 995                let line_range = {
 996                    let (start, end) = line_args
 997                        .trim_start_matches('(')
 998                        .trim_end_matches(')')
 999                        .split_once('-')?;
1000                    start.parse::<usize>().ok()?..end.parse::<usize>().ok()?
1001                };
1002
1003                Some(MentionLink::Selection(project_path, line_range))
1004            }
1005            Self::THREAD => {
1006                if let Some(encoded_filename) = argument.strip_prefix(Self::TEXT_THREAD_URL_PREFIX)
1007                {
1008                    let filename = urlencoding::decode(encoded_filename).ok()?;
1009                    let path = contexts_dir().join(filename.as_ref()).into();
1010                    Some(MentionLink::TextThread(path))
1011                } else {
1012                    let thread_id = ThreadId::from(argument);
1013                    Some(MentionLink::Thread(thread_id))
1014                }
1015            }
1016            Self::FETCH => Some(MentionLink::Fetch(argument.to_string())),
1017            Self::RULE => {
1018                let prompt_id = UserPromptId(Uuid::try_parse(argument).ok()?);
1019                Some(MentionLink::Rule(prompt_id))
1020            }
1021            _ => None,
1022        }
1023    }
1024}