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