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